+ * Send a certificate signing certificateSigningRequest to IoT hub and receive the signed certificates back.
+ *
+ *
+ * This is a multi-step process:
+ * - This client sends the certificate signing certificateSigningRequest to IoT hub
+ * - IoT hub will quickly send a response message that describes if the certificateSigningRequest was accepted or not
+ * - If accepted, IoT hub will go through the certificate signing process. Once completed, IoT hub will send another message back to this client with the signed certificates.
+ *
+ * The user-provided callback will be notified when each of these steps has finished.
+ *
+ *
+ * To instead be notified via futures, see {@link #sendCertificateSigningRequestAsync(IotHubCertificateSigningRequest)}
+ *
+ * @param certificateSigningRequest The certificate signing certificateSigningRequest to make of IoT hub.
+ * @param callback The callback that will notify you for each important step in this process.
+ */
+ public void sendCertificateSigningRequestAsync(IotHubCertificateSigningRequest certificateSigningRequest, IotHubCertificateSigningResponseCallback callback)
+ {
+ if (this.config.getProtocol() != IotHubClientProtocol.MQTT && this.config.getProtocol() != IotHubClientProtocol.MQTT_WS)
+ {
+ throw new UnsupportedOperationException("Certificate signing is only supported over MQTT or MQTT_WS");
+ }
+
+ // This one message signals to lower layers to both subscribe to MQTT response topic (if not already subscribed)
+ // and to send the CSR. This is a bit different from how methods/twins work but vastly simplifies the user
+ // experience here (compared to having a separate method for subscribing to CSR response topic).
+ IotHubTransportMessage message = new IotHubTransportMessage(certificateSigningRequest.toJson());
+ message.setDeviceOperationType(DeviceOperations.DEVICE_OPERATION_CERTIFICATE_SIGNING_REQUEST);
+ message.setIotHubCertificateSigningResponseCallback(callback);
+ message.setMessageType(MessageType.CERTIFICATE_SIGNING);
+ message.setRequestId(certificateSigningRequest.getRequestId());
+
+ this.getDeviceIO().sendEventAsync(message, null, null, this.config.getDeviceId());
+ }
+
+ /**
+ *
+ * Send a certificate signing certificateSigningRequest to IoT hub and receive the signed certificates back.
+ *
+ *
+ * This is a multi-step process:
+ * - This client sends the certificate signing certificateSigningRequest to IoT hub
+ * - IoT hub will quickly send a response message that describes if the certificateSigningRequest was accepted or not
+ * - If accepted, IoT hub will go through the certificate signing process. Once completed, IoT hub will send another message back to this client with the signed certificates.
+ *
+ * Each future in the returned collection will be completed when each corresponding step has finished.
+ *
+ *
+ * To instead be notified via callback, see {@link #sendCertificateSigningRequestAsync(IotHubCertificateSigningRequest,IotHubCertificateSigningResponseCallback)}
+ *
+ * @param certificateSigningRequest The certificate signing certificateSigningRequest to make of IoT hub.
+ * @return A collection of the futures that will complete once each corresponding step in this process has completed.
+ */
+ public IotHubCertificateSigningResponseFutures sendCertificateSigningRequestAsync(IotHubCertificateSigningRequest certificateSigningRequest)
+ {
+ IotHubCertificateSigningResponseFutures responses = new IotHubCertificateSigningResponseFutures();
+ CompletableFuture acceptedFuture = new CompletableFuture<>();
+ CompletableFuture responseFuture = new CompletableFuture<>();
+
+ responses.setOnCertificateSigningRequestAccepted(acceptedFuture);
+ responses.setOnCertificateSigningCompleted(responseFuture);
+
+ this.sendCertificateSigningRequestAsync(certificateSigningRequest, new IotHubCertificateSigningResponseCallback()
+ {
+ @Override
+ public void onCertificateSigningRequestAccepted(IotHubCertificateSigningRequestAccepted accepted)
+ {
+ acceptedFuture.complete(accepted);
+ }
+
+ @Override
+ public void onCertificateSigningComplete(IotHubCertificateSigningResponse response)
+ {
+ responseFuture.complete(response);
+ }
+
+ @Override
+ public void onCertificateSigningError(IotHubCertificateSigningError error)
+ {
+ acceptedFuture.completeExceptionally(new IotHubCertificateSigningException(error.getMessage(), error));
+ responseFuture.completeExceptionally(new IotHubCertificateSigningException(error.getMessage(), error));
+ }
+ });
+
+ return responses;
+ }
+
// Used by multiplexing clients to signal to this client what kind of multiplexing client is using this device client
void markAsMultiplexed()
{
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/MessageType.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/MessageType.java
index 2cc1bbfba7..4a9a0c8773 100644
--- a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/MessageType.java
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/MessageType.java
@@ -11,5 +11,6 @@ public enum MessageType
UNKNOWN,
DEVICE_TELEMETRY,
DEVICE_METHODS,
- DEVICE_TWIN
+ DEVICE_TWIN,
+ CERTIFICATE_SIGNING
}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningError.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningError.java
new file mode 100644
index 0000000000..40119df5d3
--- /dev/null
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningError.java
@@ -0,0 +1,97 @@
+package com.microsoft.azure.sdk.iot.device.certificatesigning;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.SerializedName;
+import com.microsoft.azure.sdk.iot.device.twin.ParserUtility;
+import lombok.Getter;
+
+import java.util.Date;
+
+/**
+ * The error reported by IoT hub if certificate signing fails.
+ */
+public class IotHubCertificateSigningError
+{
+/* Example:
+{
+ "errorCode": 400040,
+ "message": "Credential management operation failed",
+ "trackingId": "59b2922c-f1c9-451b-b02d-5b64bc31685a",
+ "timestampUtc": "2025-06-09T17:31:31.426574675Z",
+ "info": {
+ "correlationId": "8819e8d8-1324-4a9c-acde-ce0318e93f31",
+ "credentialError": "FailedToDecodeCsr",
+ "credentialMessage": "Failed to decode CSR: invalid base64 encoding"
+ }
+}
+*/
+ @SerializedName("errorCode")
+ private String errorCodeString;
+
+ /**
+ * The error code that explains why the operation failed.
+ */
+ @Getter
+ private transient IotHubCertificateSigningErrorCode errorCode;
+
+ /**
+ * The human readable error message
+ */
+ @SerializedName("message")
+ @Getter
+ private String message;
+
+ /**
+ * The tracking Id associated with this failure. If you request support for this failure, please include this tracking Id.
+ */
+ @SerializedName("trackingId")
+ @Getter
+ private String trackingId;
+
+ @SerializedName("timestampUtc")
+ private String timestampUtcString;
+
+ /**
+ * The UTC time at which this error happened.
+ */
+ @Getter
+ private transient Date timestampUtc;
+
+ /**
+ * Further information about this error.
+ */
+ @SerializedName("info")
+ @Getter
+ private IotHubCertificateSigningErrorInfo info;
+
+ public IotHubCertificateSigningError(String json) throws IllegalArgumentException
+ {
+ Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create();
+ IotHubCertificateSigningError deserialized;
+
+ ParserUtility.validateStringUTF8(json);
+ try
+ {
+ deserialized = gson.fromJson(json, IotHubCertificateSigningError.class);
+ }
+ catch (JsonSyntaxException malformed)
+ {
+ throw new IllegalArgumentException("Malformed json", malformed);
+ }
+
+ this.errorCodeString = deserialized.errorCodeString;
+ this.errorCode = IotHubCertificateSigningErrorCode.GetValue(Integer.parseInt(this.errorCodeString));
+ this.trackingId = deserialized.trackingId;
+ this.message = deserialized.message;
+ this.timestampUtcString = deserialized.timestampUtcString;
+ this.timestampUtc = ParserUtility.getDateTimeUtc(this.timestampUtcString);
+ this.info = deserialized.getInfo();
+ }
+
+ @SuppressWarnings("unused") // used by gson
+ IotHubCertificateSigningError()
+ {
+ }
+}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningErrorCode.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningErrorCode.java
new file mode 100644
index 0000000000..0d65826374
--- /dev/null
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningErrorCode.java
@@ -0,0 +1,58 @@
+package com.microsoft.azure.sdk.iot.device.certificatesigning;
+
+public enum IotHubCertificateSigningErrorCode
+{
+ /**
+ * This error code should never happen since this SDK hardcodes the protocol version to a valid version
+ */
+ InvalidProtocolVersion,
+
+ OperationNotAvailableInCurrentTier,
+ PreconditionFailed,
+ CredentialManagementPreconditionFailed,
+ ThrottleBacklogLimitExceeded,
+ ThrottlingBacklogTimeout,
+ CredentialOperationPending,
+ CredentialOperationActive,
+ CredentialOperationFailed,
+ DeviceNotFound,
+ DeviceUnavailable,
+ ServerError,
+ ServiceUnavailable,
+ Unknown;
+
+ public static IotHubCertificateSigningErrorCode GetValue(int code)
+ {
+ switch (code)
+ {
+ case 400001:
+ return InvalidProtocolVersion;
+ case 403010:
+ return OperationNotAvailableInCurrentTier;
+ case 412001:
+ return PreconditionFailed;
+ case 412005:
+ return CredentialManagementPreconditionFailed;
+ case 429002:
+ return ThrottleBacklogLimitExceeded;
+ case 429003:
+ return ThrottlingBacklogTimeout;
+ case 409004:
+ return CredentialOperationPending;
+ case 409005:
+ return CredentialOperationActive;
+ case 400040:
+ return CredentialOperationFailed;
+ case 404001:
+ return DeviceNotFound;
+ case 503102:
+ return DeviceUnavailable;
+ case 500001:
+ return ServerError;
+ case 503001:
+ return ServiceUnavailable;
+ default:
+ return Unknown;
+ }
+ }
+}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningErrorInfo.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningErrorInfo.java
new file mode 100644
index 0000000000..de60bcd5a6
--- /dev/null
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningErrorInfo.java
@@ -0,0 +1,104 @@
+package com.microsoft.azure.sdk.iot.device.certificatesigning;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.SerializedName;
+import com.microsoft.azure.sdk.iot.device.twin.ParserUtility;
+import lombok.Getter;
+
+import java.util.Date;
+
+/**
+ *
+ * Additional context for why a certificate signing operation failed.
+ *
+ *
+ * Depending on the type of error, some fields will be present and others will not. For example, if the error was that
+ * certificate signing failed because a certificate signing operation was already in progress, {@link #operationExpires}
+ * and {@link #requestId} will be present.
+ *
+ */
+public class IotHubCertificateSigningErrorInfo
+{
+
+ /* Example:
+ {
+ "correlationId": "8819e8d8-1324-4a9c-acde-ce0318e93f31",
+ "credentialError": "FailedToDecodeCsr",
+ "credentialMessage": "Failed to decode CSR: invalid base64 encoding"
+ }
+
+ alternatively, in the case of an "operation already in progress" error:
+ {
+ "requestId": "aabbcc",
+ "correlationId": "8819e8d8-1324-4a9c-acde-ce0318e93f31",
+ "operationExpires": "2025-06-09T17:31:31.426Z"
+ }
+ */
+
+ /**
+ * The correlation Id associated with this certificate signing request. For diagnostic purposes only.
+ */
+ @SerializedName("correlationId")
+ @Getter
+ private String correlationId;
+
+ /**
+ * The credential error code
+ */
+ @SerializedName("credentialError")
+ @Getter
+ private String credentialError;
+
+ /**
+ * The human readable credential error message
+ */
+ @SerializedName("credentialMessage")
+ @Getter
+ private String credentialMessage;
+
+ /**
+ * The request Id associated with this certificate signing request failure
+ */
+ @SerializedName("requestId")
+ private String requestId;
+
+ /**
+ * Only present if this error details a "certificate signing operation already in progress" error. This value is
+ * when the already-in-progress certificate signing operation for this device will expire.
+ */
+ @SerializedName("operationExpires")
+ private String operationExpiresString;
+
+ @Getter
+ private transient Date operationExpires;
+
+ public IotHubCertificateSigningErrorInfo(String json) throws IllegalArgumentException
+ {
+ Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create();
+ IotHubCertificateSigningErrorInfo deserialized;
+
+ ParserUtility.validateStringUTF8(json);
+ try
+ {
+ deserialized = gson.fromJson(json, IotHubCertificateSigningErrorInfo.class);
+ }
+ catch (JsonSyntaxException malformed)
+ {
+ throw new IllegalArgumentException("Malformed json", malformed);
+ }
+
+ this.correlationId = deserialized.correlationId;
+ this.credentialError = deserialized.credentialError;
+ this.credentialMessage = deserialized.credentialMessage;
+ this.operationExpiresString = deserialized.operationExpiresString;
+ this.operationExpires = ParserUtility.getDateTimeUtc(this.operationExpiresString);
+ this.requestId = deserialized.requestId;
+ }
+
+ @SuppressWarnings("unused") // used by gson
+ IotHubCertificateSigningErrorInfo()
+ {
+ }
+}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningException.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningException.java
new file mode 100644
index 0000000000..da06c28c7d
--- /dev/null
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningException.java
@@ -0,0 +1,20 @@
+package com.microsoft.azure.sdk.iot.device.certificatesigning;
+
+import lombok.Getter;
+
+/**
+ * IoT hub reported an error during a certificate signing request..
+ */
+public class IotHubCertificateSigningException extends Exception
+{
+ /**
+ * The error reported by IoT hub that caused the certificate signing to fail.
+ */
+ @Getter
+ private IotHubCertificateSigningError error;
+
+ public IotHubCertificateSigningException(String message, IotHubCertificateSigningError error)
+ {
+ super(message);
+ }
+}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningRequest.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningRequest.java
new file mode 100644
index 0000000000..e648993b24
--- /dev/null
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningRequest.java
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+package com.microsoft.azure.sdk.iot.device.certificatesigning;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+
+import java.util.UUID;
+
+public class IotHubCertificateSigningRequest
+{
+ /**
+ * Required. The device ID the certificate will be issued for.
+ * Must match the currently authenticated device ID.
+ */
+ @SerializedName("id")
+ private String id = null;
+
+ /**
+ * The Base64-encoded PKCS#10 CSR without PEM headers/footers or newlines.
+ */
+ @SerializedName("csr")
+ private String certificateSigningRequest = null;
+
+ /**
+ * Optional. Request ID to replace, or "*" to replace any active request.
+ * Use when:
+ * - The CSR is known to be different from a previous incomplete request
+ * - Client received 409005 and doesn't know if CSR has changed (e.g., storage failure)
+ * Default: null (will fail with 409005 if an active operation exists)
+ */
+ @SerializedName("replace")
+ private String replace = null;
+
+ /**
+ * The randomly generated request Id associated with this certificate signing request.
+ */
+ @Getter
+ private final transient String requestId;
+
+ /**
+ * Create a certificate signing request that will fail if any certificate signing requests for this device are already in progress.
+ *
+ * @param id The device ID the certificate will be issued for. Must match the device Id of the device that will send this request.
+ * @param certificateSigningRequest The Base64-encoded PKCS#10 CSR without PEM headers/footers or newlines.
+ */
+ public IotHubCertificateSigningRequest(String id, String certificateSigningRequest)
+ {
+ this(id, certificateSigningRequest, null);
+ }
+
+ /**
+ * Create a certificate signing request that will be accepted by IoT hub depending on the provided "replace" value and depending on
+ * if any certificate signing requests for this device are already in progress.
+ *
+ * @param id The device ID the certificate will be issued for. Must match the device Id of the device that will send this request.
+ * @param certificateSigningRequest The Base64-encoded PKCS#10 CSR without PEM headers/footers or newlines.
+ * @param replace the request ID to replace, or "*" to replace any active request. For use if a
+ * previous certificate signing request has failed and you want to start over.
+ */
+ public IotHubCertificateSigningRequest(String id, String certificateSigningRequest, String replace)
+ {
+ if (id == null || id.isEmpty())
+ {
+ throw new IllegalArgumentException("Id must be non-null and not empty");
+ }
+
+ if (certificateSigningRequest == null || certificateSigningRequest.isEmpty())
+ {
+ throw new IllegalArgumentException("certificateSigningRequestData must be non-null and not empty");
+ }
+
+ this.id = id;
+ this.certificateSigningRequest = certificateSigningRequest;
+ this.replace = replace;
+ this.requestId = UUID.randomUUID().toString();
+ }
+
+ public String toJson()
+ {
+ Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create();
+
+ return gson.toJson(this);
+ }
+
+ @SuppressWarnings("unused") // used by gson
+ IotHubCertificateSigningRequest()
+ {
+ this.requestId = UUID.randomUUID().toString();
+ }
+}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningRequestAccepted.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningRequestAccepted.java
new file mode 100644
index 0000000000..2a36618261
--- /dev/null
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningRequestAccepted.java
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+package com.microsoft.azure.sdk.iot.device.certificatesigning;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.SerializedName;
+import com.microsoft.azure.sdk.iot.device.twin.ParserUtility;
+import lombok.Getter;
+
+import java.util.Date;
+
+/**
+ * The information provided from IoT Hub when it accepts a certificate signing request.
+ */
+public class IotHubCertificateSigningRequestAccepted
+{
+ /**
+ * The correlation Id for this certificate signing request flow. For diagnostic purposes only.
+ */
+ @SerializedName("correlationId")
+ @Getter
+ private String correlationId;
+
+ @SerializedName("operationExpires")
+ private String operationExpiresString;
+
+ /**
+ * The UTC time at which this accepted certificate signing request will have expired if IoT Hub does not send any further updates.
+ */
+ @Getter
+ private transient Date operationExpires;
+
+ public IotHubCertificateSigningRequestAccepted(String json) throws IllegalArgumentException
+ {
+ Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create();
+ IotHubCertificateSigningRequestAccepted deserialized;
+
+ ParserUtility.validateStringUTF8(json);
+ try
+ {
+ deserialized = gson.fromJson(json, IotHubCertificateSigningRequestAccepted.class);
+ }
+ catch (JsonSyntaxException malformed)
+ {
+ throw new IllegalArgumentException("Malformed json", malformed);
+ }
+
+ this.operationExpires = ParserUtility.getDateTimeUtc(deserialized.operationExpiresString);
+ this.correlationId = deserialized.correlationId;
+ }
+
+ @SuppressWarnings("unused") // used by gson
+ IotHubCertificateSigningRequestAccepted()
+ {
+ }
+}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningResponse.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningResponse.java
new file mode 100644
index 0000000000..1cf5b4048d
--- /dev/null
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningResponse.java
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+package com.microsoft.azure.sdk.iot.device.certificatesigning;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.SerializedName;
+import com.microsoft.azure.sdk.iot.device.twin.ParserUtility;
+import lombok.Getter;
+
+import java.util.List;
+
+/**
+ * The response message from IoT hub containing the signed certificates.
+ */
+public class IotHubCertificateSigningResponse
+{
+ /**
+ *
+ * List of Base64-encoded certificates in the certificate chain.
+ *
+ *
+ * The first certificate is the issued device certificate, followed by intermediates.
+ *
+ */
+ @SerializedName("certificates")
+ @Getter
+ private List certificates;
+
+ /**
+ * Correlation ID for diagnostic and support purposes.
+ */
+ @SerializedName("correlationId")
+ @Getter
+ private String correlationId;
+
+ public IotHubCertificateSigningResponse(String json) throws IllegalArgumentException
+ {
+ Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create();
+ IotHubCertificateSigningResponse deserialized;
+
+ ParserUtility.validateStringUTF8(json);
+ try
+ {
+ deserialized = gson.fromJson(json, IotHubCertificateSigningResponse.class);
+ }
+ catch (JsonSyntaxException malformed)
+ {
+ throw new IllegalArgumentException("Malformed json", malformed);
+ }
+
+ this.correlationId = deserialized.correlationId;
+ this.certificates = deserialized.certificates;
+ }
+
+ @SuppressWarnings("unused") // used by gson
+ IotHubCertificateSigningResponse()
+ {
+ }
+}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningResponseCallback.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningResponseCallback.java
new file mode 100644
index 0000000000..bf3e3f7871
--- /dev/null
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningResponseCallback.java
@@ -0,0 +1,49 @@
+package com.microsoft.azure.sdk.iot.device.certificatesigning;
+
+/**
+ * The callback for each stage of making a certificate signing request to IoT Hub.
+ */
+public interface IotHubCertificateSigningResponseCallback
+{
+ /**
+ *
+ * Executes if/when IoT hub sends a 202 in response to the certificate signing request.
+ *
+ *
+ * When this executes, it signals that IoT hub has begun processing the certificate signing request and
+ * will notify this client again once that request has been completed via {@link #onCertificateSigningComplete(IotHubCertificateSigningResponse)} ()}
+ *
+ *
+ * If the certificate signing request is not accetepted or fails for any reason, {@link #onCertificateSigningError(IotHubCertificateSigningError)} ()}
+ * will execute instead of this callback.
+ *
+ * @param accepted The response message from IoT hub saying that the certificate signing request was accepted.
+ */
+ public void onCertificateSigningRequestAccepted(IotHubCertificateSigningRequestAccepted accepted);
+
+ /**
+ *
+ * Executes if/when IoT hub sends a 200 in response to the certificate signing request.
+ *
+ *
+ * When this executes, it signals that IoT hub has completed signing the certificates.
+ *
+ *
+ * If the certificate signing request cannot be completed or fails for any reason, {@link #onCertificateSigningError(IotHubCertificateSigningError)} ()}
+ * will execute instead of this callback.
+ *
+ * @param response The signed certificates
+ */
+ public void onCertificateSigningComplete(IotHubCertificateSigningResponse response);
+
+ /**
+ *
+ * Executes if/when IoT hub sends a response to the certificate signing request, but it is neither a 202 nor a 200.
+ *
+ *
+ * This callback may execute even after a certificate signing request has been accepted.
+ *
+ * @param error details on why this certificate signing request failed.
+ */
+ public void onCertificateSigningError(IotHubCertificateSigningError error);
+}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningResponseFutures.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningResponseFutures.java
new file mode 100644
index 0000000000..13964815f7
--- /dev/null
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/certificatesigning/IotHubCertificateSigningResponseFutures.java
@@ -0,0 +1,36 @@
+package com.microsoft.azure.sdk.iot.device.certificatesigning;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.concurrent.Future;
+
+/**
+ * A collection of futures that complete at each stage of the certificate signing process
+ */
+public class IotHubCertificateSigningResponseFutures
+{
+ /**
+ *
+ * This future will complete once IoT hub has accepted the certificate signing request.
+ *
+ *
+ * If IoT hub instead responds with an error, this future will complete exceptionally with a {@link IotHubCertificateSigningException}.
+ *
+ * This future will complete once IoT hub has finished signing the certificates and has sent them back to this client.
+ *
+ *
+ * If IoT hub instead responds with an error, this future will complete exceptionally with a {@link IotHubCertificateSigningException}.
+ *
+ */
+ @Getter
+ @Setter
+ Future OnCertificateSigningCompleted;
+}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/IotHubTransportMessage.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/IotHubTransportMessage.java
index 4249c33ec0..159ddf89a1 100644
--- a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/IotHubTransportMessage.java
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/IotHubTransportMessage.java
@@ -3,6 +3,7 @@
package com.microsoft.azure.sdk.iot.device.transport;
+import com.microsoft.azure.sdk.iot.device.certificatesigning.IotHubCertificateSigningResponseCallback;
import com.microsoft.azure.sdk.iot.device.twin.DeviceOperations;
import com.microsoft.azure.sdk.iot.device.*;
import com.microsoft.azure.sdk.iot.device.transport.https.HttpsMethod;
@@ -36,6 +37,10 @@ public class IotHubTransportMessage extends Message
@Setter
private int qualityOfService;
+ @Getter
+ @Setter
+ private IotHubCertificateSigningResponseCallback iotHubCertificateSigningResponseCallback;
+
/**
* Constructor with binary data and message type
* @param data The byte array of the message.
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/TransportUtils.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/TransportUtils.java
index 56660c8bf5..dd00e2eac0 100644
--- a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/TransportUtils.java
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/TransportUtils.java
@@ -15,7 +15,7 @@
@Slf4j
public class TransportUtils
{
- public static final String IOTHUB_API_VERSION = "2020-09-30";
+ public static final String IOTHUB_API_VERSION = "2025-08-01-preview";
private static final String JAVA_DEVICE_CLIENT_IDENTIFIER = "com.microsoft.azure.sdk.iot.iot-device-client";
public static final String CLIENT_VERSION = getPackageVersion();
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/mqtt/MqttCertificateSigning.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/mqtt/MqttCertificateSigning.java
new file mode 100644
index 0000000000..3e01eada69
--- /dev/null
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/mqtt/MqttCertificateSigning.java
@@ -0,0 +1,191 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+package com.microsoft.azure.sdk.iot.device.transport.mqtt;
+
+import com.microsoft.azure.sdk.iot.device.*;
+import com.microsoft.azure.sdk.iot.device.certificatesigning.IotHubCertificateSigningError;
+import com.microsoft.azure.sdk.iot.device.certificatesigning.IotHubCertificateSigningRequestAccepted;
+import com.microsoft.azure.sdk.iot.device.certificatesigning.IotHubCertificateSigningResponse;
+import com.microsoft.azure.sdk.iot.device.certificatesigning.IotHubCertificateSigningResponseCallback;
+import com.microsoft.azure.sdk.iot.device.transport.IotHubTransportMessage;
+import com.microsoft.azure.sdk.iot.device.transport.TransportException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.tuple.Pair;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+import java.util.regex.Pattern;
+
+@Slf4j
+class MqttCertificateSigning extends Mqtt
+{
+ private final String certificateSigningResponseTopic = "$iothub/credentials/res/#";
+ private boolean isStarted = false;
+ private Map inProgressRequestIdMap = new HashMap<>();
+ private static final String REQ_ID = "?$rid=";
+
+ public MqttCertificateSigning(
+ String deviceId,
+ MqttConnectOptions connectOptions,
+ Map unacknowledgedSentMessages,
+ Queue> receivedMessages)
+ {
+ super(null, deviceId, connectOptions, unacknowledgedSentMessages, receivedMessages);
+
+ if (deviceId == null || deviceId.isEmpty())
+ {
+ throw new IllegalArgumentException("Device id cannot be null or empty");
+ }
+ }
+
+ public void start() throws TransportException
+ {
+ // TODO how well does this handle resumed sessions/lost sessions?
+ if (!this.isStarted)
+ {
+ this.subscribe(this.certificateSigningResponseTopic);
+ this.isStarted = true;
+ }
+ }
+
+ public void stop()
+ {
+ this.isStarted = false;
+ }
+
+ public void send(IotHubTransportMessage message) throws TransportException
+ {
+ IotHubCertificateSigningResponseCallback signingCallback = message.getIotHubCertificateSigningResponseCallback();
+
+ // Service will ack this immediately, then later publish a message to the response topic
+ inProgressRequestIdMap.put(message.getRequestId(), signingCallback);
+ this.publish("$iothub/credentials/POST/issueCertificate/?$rid=" + message.getRequestId(), message);
+
+ // IoT hub will respond to this request by sending a few response messages over the subscribed topic.
+ }
+
+ @Override
+ public IotHubTransportMessage receive()
+ {
+ synchronized (this.receivedMessagesLock)
+ {
+ IotHubTransportMessage message = null;
+
+ Pair messagePair = this.receivedMessages.peek();
+
+ if (messagePair != null)
+ {
+ String topic = messagePair.getKey();
+
+ if (topic != null && topic.length() > 0)
+ {
+ //$iothub/credentials/res/{status}/?$rid={request_id}
+ if (topic.startsWith("$iothub/credentials/res/"))
+ {
+ MqttMessage mqttMessage = messagePair.getValue();
+ byte[] payload = mqttMessage.getPayload();
+
+ //remove this message from the queue as this is the correct handler
+ this.receivedMessages.poll();
+
+ String[] topicTokens = topic.split(Pattern.quote("/"));
+ if (topicTokens.length == 5)
+ {
+ String status = topicTokens[3];
+ String requestId = getRequestId(topicTokens[4]);
+ if (this.inProgressRequestIdMap.containsKey(requestId))
+ {
+ IotHubCertificateSigningResponseCallback iotHubCertificateSigningResponseCallback = this.inProgressRequestIdMap.get(requestId);
+
+ if (status.equals("202"))
+ {
+ try
+ {
+ IotHubCertificateSigningRequestAccepted accepted = new IotHubCertificateSigningRequestAccepted(new String(payload, StandardCharsets.UTF_8));
+ iotHubCertificateSigningResponseCallback.onCertificateSigningRequestAccepted(accepted);
+ IotHubTransportMessage transportMessage = new IotHubTransportMessage(new byte[0], MessageType.CERTIFICATE_SIGNING);
+ transportMessage.setMessageType(MessageType.CERTIFICATE_SIGNING);
+ transportMessage.setQualityOfService(mqttMessage.getQos());
+ return transportMessage;
+ }
+ catch (IllegalArgumentException e)
+ {
+ log.error("Received certificate signing request accepted message with malformed payload. Ignoring it.");
+ }
+ }
+ else if (status.equals("200"))
+ {
+ try
+ {
+ this.inProgressRequestIdMap.remove(requestId);
+ IotHubCertificateSigningResponse response = new IotHubCertificateSigningResponse(new String(payload, StandardCharsets.UTF_8));
+ iotHubCertificateSigningResponseCallback.onCertificateSigningComplete(response);
+ IotHubTransportMessage transportMessage = new IotHubTransportMessage(new byte[0], MessageType.CERTIFICATE_SIGNING);
+ transportMessage.setQualityOfService(mqttMessage.getQos());
+ return transportMessage;
+ }
+ catch (IllegalArgumentException e)
+ {
+ log.error("Received certificate signing response message with malformed payload. Ignoring it.");
+ }
+ }
+ else
+ {
+ try
+ {
+ this.inProgressRequestIdMap.remove(requestId);
+ IotHubCertificateSigningError error = new IotHubCertificateSigningError(new String(payload, StandardCharsets.UTF_8));
+ iotHubCertificateSigningResponseCallback.onCertificateSigningError(error);
+ IotHubTransportMessage transportMessage = new IotHubTransportMessage(new byte[0], MessageType.CERTIFICATE_SIGNING);
+ transportMessage.setQualityOfService(mqttMessage.getQos());
+ return transportMessage;
+ }
+ catch (IllegalArgumentException e)
+ {
+ log.error("Received certificate signing error message with malformed payload. Ignoring it.");
+ IotHubTransportMessage transportMessage = new IotHubTransportMessage(new byte[0], MessageType.CERTIFICATE_SIGNING);
+ transportMessage.setQualityOfService(mqttMessage.getQos());
+ return transportMessage;
+ }
+ }
+ }
+ else
+ {
+ log.warn("Received certificate signing response message for an unknown request Id. Ignoring it.");
+ IotHubTransportMessage transportMessage = new IotHubTransportMessage(new byte[0], MessageType.CERTIFICATE_SIGNING);
+ transportMessage.setQualityOfService(mqttMessage.getQos());
+ return transportMessage;
+ }
+ }
+ else
+ {
+ log.warn("Received MQTT message on certificate signing response topic with an unexpected topic pattern. Ignoring it.");
+ }
+ }
+ }
+ }
+
+ return message;
+ }
+ }
+
+ private String getRequestId(String token)
+ {
+ String reqId = null;
+
+ if (token.contains(REQ_ID)) // restriction for request id
+ {
+ int startIndex = token.indexOf(REQ_ID) + REQ_ID.length();
+ int endIndex = token.length();
+
+ reqId = token.substring(startIndex, endIndex);
+ }
+
+ return reqId;
+ }
+}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/mqtt/MqttIotHubConnection.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/mqtt/MqttIotHubConnection.java
index 51d1a4e968..4e7ef8660d 100644
--- a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/mqtt/MqttIotHubConnection.java
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/transport/mqtt/MqttIotHubConnection.java
@@ -27,8 +27,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
-import static com.microsoft.azure.sdk.iot.device.MessageType.DEVICE_METHODS;
-import static com.microsoft.azure.sdk.iot.device.MessageType.DEVICE_TWIN;
+import static com.microsoft.azure.sdk.iot.device.MessageType.*;
import static com.microsoft.azure.sdk.iot.device.transport.mqtt.Mqtt.MAX_IN_FLIGHT_COUNT;
import static org.eclipse.paho.client.mqttv3.MqttConnectOptions.MQTT_VERSION_3_1_1;
@@ -62,6 +61,7 @@ public class MqttIotHubConnection implements IotHubTransportConnection, MqttMess
private final MqttMessaging deviceMessaging;
private final MqttTwin deviceTwin;
private final MqttDirectMethod directMethod;
+ private final MqttCertificateSigning certificateSigning;
private final Map receivedMessagesToAcknowledge = new ConcurrentHashMap<>();
@@ -237,6 +237,12 @@ else if (proxySettings.getProxy().type() == Proxy.Type.HTTP)
connectOptions,
unacknowledgedSentMessages,
receivedMessages);
+
+ this.certificateSigning = new MqttCertificateSigning(
+ deviceId,
+ connectOptions,
+ unacknowledgedSentMessages,
+ receivedMessages);
}
/**
@@ -282,6 +288,7 @@ public void open() throws TransportException
this.deviceMessaging.setMqttAsyncClient(mqttAsyncClient);
this.deviceTwin.setMqttAsyncClient(mqttAsyncClient);
this.directMethod.setMqttAsyncClient(mqttAsyncClient);
+ this.certificateSigning.setMqttAsyncClient(mqttAsyncClient);
this.deviceMessaging.start();
this.state = IotHubConnectionStatus.CONNECTED;
@@ -367,6 +374,12 @@ else if (message.getMessageType() == DEVICE_TWIN)
log.trace("Sending MQTT device twin message ({})", message);
this.deviceTwin.send((IotHubTransportMessage) message);
}
+ else if (message.getMessageType() == CERTIFICATE_SIGNING && message instanceof IotHubTransportMessage)
+ {
+ this.certificateSigning.start();
+ log.trace("Sending MQTT certificate signing request message ({})", message);
+ this.certificateSigning.send((IotHubTransportMessage) message);
+ }
else
{
log.trace("Sending MQTT device telemetry message ({})", message);
@@ -455,10 +468,18 @@ public void onMessageArrived(int messageId)
}
else
{
- transportMessage = deviceMessaging.receive();
+ transportMessage = certificateSigning.receive();
if (transportMessage != null)
{
- log.trace("Received MQTT device messaging message ({})", transportMessage);
+ log.trace("Received MQTT certificate signing message ({})", transportMessage);
+ }
+ else
+ {
+ transportMessage = deviceMessaging.receive();
+ if (transportMessage != null)
+ {
+ log.trace("Received MQTT device messaging message ({})", transportMessage);
+ }
}
}
}
diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/twin/DeviceOperations.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/twin/DeviceOperations.java
index d74b548a40..2f2d8bad1c 100644
--- a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/twin/DeviceOperations.java
+++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/twin/DeviceOperations.java
@@ -17,5 +17,6 @@ public enum DeviceOperations
DEVICE_OPERATION_METHOD_SUBSCRIBE_RESPONSE,
DEVICE_OPERATION_METHOD_RECEIVE_REQUEST,
DEVICE_OPERATION_METHOD_SEND_RESPONSE,
+ DEVICE_OPERATION_CERTIFICATE_SIGNING_REQUEST,
DEVICE_OPERATION_UNKNOWN
}
diff --git a/pom.xml b/pom.xml
index b63f3d0863..12d25a44e0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -83,7 +83,7 @@
com.fasterxml.jackson.corejackson-core
- 2.21.0
+ 2.21.1com.fasterxml.jackson.core
@@ -147,13 +147,18 @@
org.bouncycastle
- bcmail-jdk15on
- 1.70
+ bcprov-jdk18on
+ 1.83
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ 1.83org.bouncycastlebcprov-jdk15on
- 1.70
+ 1.70org.apache.logging.log4j
diff --git a/provisioning/provisioning-device-client-samples/certificate-signing-sample/README.md b/provisioning/provisioning-device-client-samples/certificate-signing-sample/README.md
new file mode 100644
index 0000000000..363e6fed30
--- /dev/null
+++ b/provisioning/provisioning-device-client-samples/certificate-signing-sample/README.md
@@ -0,0 +1,65 @@
+# DPS and IoT hub Certificate Signing Sample
+
+This sample demonstrates the certificate signing features available when provisioning a device with the Device Provisioning Service and when connected to IoT Hub.
+
+*Note that this feature is currently only available over MQTT/MQTT_WS for both DPS and IoT Hub*
+
+## Prerequisites
+
+1. **Azure IoT Hub** - An Azure IoT Hub instance
+1. **Azure Device Provisioning Service (DPS)** - Linked to your IoT Hub (for initial provisioning)
+1. **DPS Enrollment** - Configured for certificate issuance
+
+## How to run
+
+Simply fill in your specific values for the fields defined at the beginning:
+
+```java
+private static final String SAMPLE_CERTIFICATES_OUTPUT_PATH = "~/SampleCertificates";
+private static final String DPS_ID_SCOPE = "<>";
+private static final String DPS_SYMMETRIC_KEY = "<>";
+```
+
+And optionally specify other values (which have defaults):
+
+```java
+private static final String PROVISIONED_DEVICE_ID = "myCsrProvisionedDevice";
+private static final CertificateType certificateType = CertificateType.RSA; // ECC vs RSA
+
+// Certificate signing feature is currently only supported over MQTT/MQTT_WS
+private static final IotHubClientProtocol iotHubProtocol = IotHubClientProtocol.MQTT;
+private static final ProvisioningDeviceClientTransportProtocol dpsProtocol = ProvisioningDeviceClientTransportProtocol.MQTT;
+```
+
+## DPS feature demonstrated
+
+When provisioning a device, you may optionally include a certificate signing request such that the provisioned device can use those certificates when connecting to IoT hub after provisioning completes.
+
+```java
+AdditionalData provisioningAdditionalData = new AdditionalData();
+provisioningAdditionalData.setClientCertificateSigningRequest(...);
+ProvisioningDeviceClientRegistrationResult provisioningResult = provisioningDeviceClient.registerDeviceSync(provisioningAdditionalData);
+List issuedClientCertificates = provisioningResult.getIssuedClientCertificateChain();
+```
+
+*Note that the provisioning device client itself does not use these soon-to-be-signed certificates when authenticating with DPS. It must use one of the TPM/Symmetric Key/x509 authentication mechanisms detailed in other samples in this directory.*
+
+## IoT hub feature demonstrated
+
+If your device needs to renew its certificates for any reason, it can send a certificate signing request to IoT hub
+
+```java
+DeviceClient client = new DeviceClient(...);
+client.open(false);
+IotHubCertificateSigningRequest iothubCsr = new IotHubCertificateSigningRequest(...);
+... = client.sendCertificateSigningRequest(iothubCsr);
+```
+
+Once IoT hub has accepted and completed this certificate signing response, you can close the connection to IoT hub, create a new client for the device that uses these renewed certificates, and then re-open the connection.
+
+## Additional feature notes
+
+- This certificate signing feature works for both RSA and ECC certificates
+- The DPS flow is applicable for any combination of
+ - Individual enrollment vs enrollment group
+ - Symmetric key vs TPM vs x509 authentication
\ No newline at end of file
diff --git a/provisioning/provisioning-device-client-samples/certificate-signing-sample/pom.xml b/provisioning/provisioning-device-client-samples/certificate-signing-sample/pom.xml
new file mode 100644
index 0000000000..68456e1922
--- /dev/null
+++ b/provisioning/provisioning-device-client-samples/certificate-signing-sample/pom.xml
@@ -0,0 +1,68 @@
+
+
+ 4.0.0
+
+ com.microsoft.azure.sdk.iot.provisioning.samples
+ provisioning-device-client-samples
+ 1.0.0
+
+
+ certificate-signing-sample
+
+
+ 11
+ 11
+ UTF-8
+
+
+
+ com.microsoft.azure.sdk.iot.provisioning.security
+ ${security-provider-artifact-id}
+ ${security-provider-version}
+
+
+ com.microsoft.azure.sdk.iot.provisioning
+ ${provisioning-device-client-artifact-id}
+ ${provisioning-device-client-version}
+
+
+ com.microsoft.azure.sdk.iot
+ ${iot-device-client-artifact-id}
+ ${iot-device-client-version}
+
+
+ com.microsoft.azure.sdk.iot.provisioning.security
+ x509-provider
+ 2.0.2
+ compile
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 1.8
+ 1.8
+
+
+
+
+
+
\ No newline at end of file
diff --git a/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/CertificateSigningRequest.java b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/CertificateSigningRequest.java
new file mode 100644
index 0000000000..ac92137ef9
--- /dev/null
+++ b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/CertificateSigningRequest.java
@@ -0,0 +1,18 @@
+package com.microsoft.azure.sdk.iot.provisioning.samples;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import java.security.*;
+
+@AllArgsConstructor
+public class CertificateSigningRequest
+{
+ @Getter
+ private final PublicKey publicKey;
+
+ @Getter
+ private final PrivateKey privateKey;
+
+ @Getter
+ private final String base64EncodedPKCS10;
+}
diff --git a/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/CertificateSigningRequestGenerator.java b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/CertificateSigningRequestGenerator.java
new file mode 100644
index 0000000000..16cf68c8cd
--- /dev/null
+++ b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/CertificateSigningRequestGenerator.java
@@ -0,0 +1,81 @@
+package com.microsoft.azure.sdk.iot.provisioning.samples;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.pkcs.PKCS10CertificationRequest;
+import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Security;
+import org.bouncycastle.asn1.pkcs.CertificationRequest;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+import java.security.spec.*;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.security.*;
+import java.security.cert.CertificateException;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.RSAKeyGenParameterSpec;
+import java.util.Base64;
+
+import static com.microsoft.azure.sdk.iot.provisioning.samples.CertificateType.ECC;
+import static com.microsoft.azure.sdk.iot.provisioning.samples.CertificateType.RSA;
+
+public class CertificateSigningRequestGenerator
+{
+ private final String signature;
+ private final KeyPairGenerator keyGen;
+ private final String commonName;
+
+ /**
+ * @param certificateType RSA or ECC
+ * @param commonName The common name of the certificate signing request. For this sample's purposes,
+ * this value should equal the registration Id being used in DPS.
+ */
+ public CertificateSigningRequestGenerator(CertificateType certificateType, String commonName) throws CertificateException, NoSuchAlgorithmException, IOException, SignatureException, InvalidKeyException, InvalidAlgorithmParameterException
+ {
+ BouncyCastleProvider prov = new BouncyCastleProvider();
+ Security.addProvider(prov);
+
+ if (certificateType == RSA)
+ {
+ this.keyGen = KeyPairGenerator.getInstance("RSA", prov);
+ this.signature = "SHA256withRSA";
+ this.keyGen.initialize(new RSAKeyGenParameterSpec(4096, RSAKeyGenParameterSpec.F4));
+ }
+ else if (certificateType == ECC)
+ {
+ this.keyGen = KeyPairGenerator.getInstance("EC", prov);
+ this.signature = "SHA256withECDSA";
+ this.keyGen.initialize(new ECGenParameterSpec("prime256v1"));
+ }
+ else
+ {
+ throw new IllegalArgumentException("Unrecognized certificate type");
+ }
+
+ this.commonName = commonName;
+ }
+
+ public CertificateSigningRequest GenerateNewCertificateSigningRequest() throws InvalidKeyException, IOException, CertificateException, SignatureException, OperatorCreationException
+ {
+ KeyPair keypair = this.keyGen.generateKeyPair();
+ PublicKey publicKey = keypair.getPublic();
+ PrivateKey privateKey = keypair.getPrivate();
+
+ org.bouncycastle.asn1.x500.X500Name name = new X500Name("CN=" + this.commonName);
+ PKCS10CertificationRequestBuilder reqBuilder = new JcaPKCS10CertificationRequestBuilder(name, publicKey);
+ JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(this.signature);
+ PKCS10CertificationRequest csr = reqBuilder.build(signerBuilder.build(privateKey));
+
+ String base64EncodedPKCS10 = Base64.getEncoder().encodeToString(csr.getEncoded());
+ return new CertificateSigningRequest(publicKey, privateKey, base64EncodedPKCS10);
+ }
+}
diff --git a/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/CertificateType.java b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/CertificateType.java
new file mode 100644
index 0000000000..f30bdc93db
--- /dev/null
+++ b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/CertificateType.java
@@ -0,0 +1,7 @@
+package com.microsoft.azure.sdk.iot.provisioning.samples;
+
+public enum CertificateType
+{
+ ECC,
+ RSA
+}
diff --git a/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/Main.java b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/Main.java
new file mode 100644
index 0000000000..5c42c5d984
--- /dev/null
+++ b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/Main.java
@@ -0,0 +1,233 @@
+package com.microsoft.azure.sdk.iot.provisioning.samples;
+
+import com.microsoft.azure.sdk.iot.device.*;
+import com.microsoft.azure.sdk.iot.device.certificatesigning.*;
+import com.microsoft.azure.sdk.iot.device.exceptions.IotHubClientException;
+import com.microsoft.azure.sdk.iot.provisioning.device.*;
+import com.microsoft.azure.sdk.iot.provisioning.device.internal.exceptions.ProvisioningDeviceClientException;
+import com.microsoft.azure.sdk.iot.provisioning.device.internal.exceptions.ProvisioningDeviceHubException;
+import com.microsoft.azure.sdk.iot.provisioning.security.SecurityProvider;
+import com.microsoft.azure.sdk.iot.provisioning.security.SecurityProviderSymmetricKey;
+import org.bouncycastle.operator.OperatorCreationException;
+
+import javax.net.ssl.SSLContext;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.util.Base64;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class Main
+{
+ // The path to write all created certificates to
+ private static final String SAMPLE_CERTIFICATES_OUTPUT_PATH = "~/SampleCertificates";
+ private static final String DPS_ID_SCOPE = "<>";
+ private static final String ENROLLMENT_GROUP_SYMMETRIC_KEY = "";
+ private static final String PROVISIONED_DEVICE_ID = "myCsrProvisionedDevice";
+ private static final CertificateType certificateType = CertificateType.RSA; // ECC vs RSA
+
+ // Certificate signing feature is currently only supported over MQTT/MQTT_WS
+ private static final IotHubClientProtocol iotHubProtocol = IotHubClientProtocol.MQTT;
+ private static final ProvisioningDeviceClientTransportProtocol dpsProtocol = ProvisioningDeviceClientTransportProtocol.MQTT;
+
+ public static void main(String[] args)
+ throws IOException, URISyntaxException, InterruptedException, IotHubClientException, GeneralSecurityException, ProvisioningDeviceClientException, OperatorCreationException
+ {
+ CertificateSigningRequestGenerator csrGenerator =
+ new CertificateSigningRequestGenerator(certificateType, PROVISIONED_DEVICE_ID);
+
+ CertificateSigningRequest dpsCsr = csrGenerator.GenerateNewCertificateSigningRequest();
+
+ String privateKeyPem = getPrivateKeyString(dpsCsr.getPrivateKey(), certificateType);
+ WriteToFile(SAMPLE_CERTIFICATES_OUTPUT_PATH, "privateKey.pem", privateKeyPem);
+
+ SecurityProvider securityProvider = CreateSecurityProvider();
+
+ ProvisioningDeviceClient provisioningDeviceClient = ProvisioningDeviceClient.create(
+ "global.azure-devices-provisioning.net",
+ DPS_ID_SCOPE,
+ dpsProtocol,
+ securityProvider);
+
+ AdditionalData provisioningAdditionalData = new AdditionalData();
+ provisioningAdditionalData.setClientCertificateSigningRequest(dpsCsr.getBase64EncodedPKCS10());
+
+ //TODO what kinds of exceptions do we expect here if the csr was gibberish, for example?
+ ProvisioningDeviceClientRegistrationResult provisioningResult;
+ try
+ {
+ provisioningResult = provisioningDeviceClient.registerDeviceSync(provisioningAdditionalData);
+ }
+ catch (ProvisioningDeviceHubException e)
+ {
+ System.out.println("Provisioning failed with error: " + e.getMessage());
+ provisioningDeviceClient.close();
+ return;
+ }
+
+ if (provisioningResult.getProvisioningDeviceClientStatus() != ProvisioningDeviceClientStatus.PROVISIONING_DEVICE_STATUS_ASSIGNED)
+ {
+ System.out.println("Provisioning failed with status: " + provisioningResult.getProvisioningDeviceClientStatus());
+ return;
+ }
+
+ if (provisioningResult.getIssuedClientCertificateChain() == null
+ || provisioningResult.getIssuedClientCertificateChain().isEmpty())
+ {
+ System.out.println("Provisioning did not yield any issued client certificates. Did you include the certificate signing request in the provisioning request?");
+ return;
+ }
+
+ String issuedClientCertificatesPem = ConvertToPem(provisioningResult.getIssuedClientCertificateChain());
+ WriteToFile(SAMPLE_CERTIFICATES_OUTPUT_PATH, "clientCertificates.pem", issuedClientCertificatesPem);
+
+ String leafCertificatePem = ConvertToPem(provisioningResult.getIssuedClientCertificateChain().get(0));
+
+ SSLContext deviceClientSslContext = SSLContextBuilder.buildSSLContext(leafCertificatePem, privateKeyPem);
+
+ provisioningDeviceClient.close();
+ System.out.println("Provisioning finished successfully. Opening device client connection with the newly signed certificates.");
+
+ String deviceId = provisioningResult.getDeviceId();
+ String iotHubUri = provisioningResult.getIothubUri();
+
+ ClientOptions clientOptions = ClientOptions.builder().sslContext(deviceClientSslContext).build();
+ String derivedConnectionString = String.format("HostName=%s;DeviceId=%s;x509=true", iotHubUri, deviceId);
+ DeviceClient client = new DeviceClient(derivedConnectionString, iotHubProtocol, clientOptions);
+
+ client.open(true);
+
+ client.sendEvent(new Message("Hello from the CSR sample!"));
+
+
+ System.out.println("Creating new CSR to send to IoT hub.");
+ CertificateSigningRequest renewalCsr = csrGenerator.GenerateNewCertificateSigningRequest();
+
+ IotHubCertificateSigningRequest iothubCsr =
+ new IotHubCertificateSigningRequest(deviceId, renewalCsr.getBase64EncodedPKCS10(), "*");
+
+ System.out.println("Sending new CSR to IoT hub.");
+ IotHubCertificateSigningResponseFutures csrResponseFutures = client.sendCertificateSigningRequestAsync(iothubCsr);
+
+ IotHubCertificateSigningResponse response;
+ IotHubCertificateSigningRequestAccepted accepted;
+
+ try
+ {
+ accepted = csrResponseFutures.getOnCertificateSigningRequestAccepted().get();
+ System.out.println("The certificate signing request was accepted by Iot Hub. Operation will expire at: " + accepted.getOperationExpires());
+ response = csrResponseFutures.getOnCertificateSigningCompleted().get();
+ System.out.println("Iot Hub completed the certificate signing request.");
+ }
+ catch (ExecutionException e)
+ {
+ //TODO I believe this should always be the case, but double check this
+ IotHubCertificateSigningException ex = (IotHubCertificateSigningException) e.getCause();
+ System.out.println("Encountered an issue while renewing the certificates: " + ex.getMessage());
+ return;
+ }
+
+ String renewedClientCertificatesPem = ConvertToPem(response.getCertificates());
+ WriteToFile(SAMPLE_CERTIFICATES_OUTPUT_PATH, "clientCertificates.pem", renewedClientCertificatesPem);
+
+ String renewedLeafCertificatePem = ConvertToPem(provisioningResult.getIssuedClientCertificateChain().get(0));
+
+ System.out.println("Closing the connection to IoT hub and reconnecting with the newly signed certificates instead...");
+
+ client.close();
+
+ SSLContext renewedDeviceClientSslContext = SSLContextBuilder.buildSSLContext(renewedLeafCertificatePem, privateKeyPem);
+ ClientOptions renewedClientOptions = ClientOptions.builder().sslContext(renewedDeviceClientSslContext).build();
+ client = new DeviceClient(derivedConnectionString, iotHubProtocol, renewedClientOptions);
+
+ client.open(true);
+
+ System.out.println("Successfully opened the connection with the newly signed certificates!");
+
+ client.sendEvent(new Message("Hello from the CSR sample!"));
+
+ client.close();
+
+ System.out.println("Done.");
+ }
+
+ // This sample can use any combination of individual enrollment vs enrollment group and TPM vs Symmetric Key vs x509 auth.
+ // For simpicity in demonstrating the CSR feature, though, this sample will use Symmetric Key + individual enrollment.
+ private static SecurityProviderSymmetricKey CreateSecurityProvider() throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ byte[] derivedSymmetricKey =
+ SecurityProviderSymmetricKey
+ .ComputeDerivedSymmetricKey(
+ ENROLLMENT_GROUP_SYMMETRIC_KEY.getBytes(StandardCharsets.UTF_8),
+ PROVISIONED_DEVICE_ID);
+
+ return new SecurityProviderSymmetricKey(derivedSymmetricKey, PROVISIONED_DEVICE_ID);
+ }
+
+ private static String getPrivateKeyString(PrivateKey privateKey, CertificateType certificateType) throws IOException
+ {
+ StringBuilder privateKeyStringBuilder = new StringBuilder();
+ if (certificateType == CertificateType.RSA)
+ {
+ privateKeyStringBuilder.append("-----BEGIN PRIVATE KEY-----\r\n");
+ }
+ else if (certificateType == CertificateType.ECC)
+ {
+ privateKeyStringBuilder.append("-----BEGIN EC PRIVATE KEY-----\r\n");
+ }
+ String privateKeyBase64Encoded = Base64.getEncoder().encodeToString(privateKey.getEncoded());
+ privateKeyStringBuilder.append(privateKeyBase64Encoded);
+ privateKeyStringBuilder.append("\r\n");
+ if (certificateType == CertificateType.RSA)
+ {
+ privateKeyStringBuilder.append("-----END PRIVATE KEY-----\r\n");
+ }
+ else if (certificateType == CertificateType.ECC)
+ {
+ privateKeyStringBuilder.append("-----END EC PRIVATE KEY-----\r\n");
+ }
+ return privateKeyStringBuilder.toString();
+ }
+
+ private static String ConvertToPem(List issuedClientCertificates)
+ {
+ StringBuilder pemBuilder = new StringBuilder();
+ for (String issuedClientCertificate : issuedClientCertificates)
+ {
+ pemBuilder.append("-----BEGIN CERTIFICATE-----\r\n");
+ pemBuilder.append(issuedClientCertificate);
+ pemBuilder.append("\r\n");
+ pemBuilder.append("-----END CERTIFICATE-----\r\n");
+ }
+
+ return pemBuilder.toString();
+ }
+
+ private static String ConvertToPem(String issuedLeafCertificate)
+ {
+ StringBuilder pemBuilder = new StringBuilder();
+ pemBuilder.append("-----BEGIN CERTIFICATE-----\r\n");
+ pemBuilder.append(issuedLeafCertificate);
+ pemBuilder.append("\r\n");
+ pemBuilder.append("-----END CERTIFICATE-----\r\n");
+
+ return pemBuilder.toString();
+ }
+
+ private static void WriteToFile(String path, String filename, String contents) throws IOException
+ {
+ Files.deleteIfExists(Paths.get(path, filename));
+ BufferedWriter writer = new BufferedWriter(new FileWriter(path + "/" + filename));
+ writer.write(contents);
+ writer.close();
+ }
+}
\ No newline at end of file
diff --git a/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/SSLContextBuilder.java b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/SSLContextBuilder.java
new file mode 100644
index 0000000000..092788b0a1
--- /dev/null
+++ b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/java/com/microsoft/azure/sdk/iot/provisioning/samples/SSLContextBuilder.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) Microsoft. All rights reserved.
+ * Licensed under the MIT license. See LICENSE file in the project root for full license information.
+ */
+
+package com.microsoft.azure.sdk.iot.provisioning.samples;
+
+import org.apache.commons.codec.binary.Base64;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Helper class that demonstrates how to build an SSLContext for x509 authentication from your public and private certificates,
+ * or how to build an SSLContext for SAS authentication from the default IoT Hub public certificates
+ */
+public class SSLContextBuilder
+{
+ private static final String SSL_CONTEXT_INSTANCE = "TLSv1.2";
+ private static final String CERTIFICATE_TYPE = "X.509";
+ private static final String RSA_PRIVATE_KEY_ALGORITHM = "RSA";
+ private static final String ECC_PRIVATE_KEY_ALGORITHM = "ECDSA";
+
+ private static final String CERTIFICATE_ALIAS = "cert-alias";
+ private static final String PRIVATE_KEY_ALIAS = "key-alias";
+
+ /**
+ * Create an SSLContext instance with the provided public certificate and private key that also trusts the public
+ * certificates loaded in your device's trusted root certification authorities certificate store.
+ * @param publicKeyCertificate the public key to use for x509 authentication. Does not need to include the
+ * Iot Hub trusted certificate as it will be added automatically as long as it is
+ * in your device's trusted root certification authorities certificate store.
+ * @param privateKey The private key to use for x509 authentication
+ * @return The created SSLContext that uses the provided public key and private key
+ * @throws GeneralSecurityException If the certificate creation fails, or if the SSLContext creation using those certificates fails.
+ * @throws IOException If the certificates cannot be read.
+ */
+ public static SSLContext buildSSLContext(X509Certificate publicKeyCertificate, PrivateKey privateKey) throws GeneralSecurityException, IOException
+ {
+ char[] temporaryPassword = generateTemporaryPassword();
+
+ KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+ keystore.load(null);
+ keystore.setCertificateEntry(CERTIFICATE_ALIAS, publicKeyCertificate);
+ keystore.setKeyEntry(PRIVATE_KEY_ALIAS, privateKey, temporaryPassword, new Certificate[] {publicKeyCertificate});
+
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(keystore, temporaryPassword);
+
+ SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_INSTANCE);
+
+ // By leaving the TrustManager array null, the SSLContext will trust the certificates stored on your device's
+ // trusted root certification authorities certificate store.
+ //
+ // This must include the Baltimore CyberTrust Root public certificate: https://baltimore-cybertrust-root.chain-demos.digicert.com/info/index.html
+ // and eventually it will need to include the DigiCert Global Root G2 public certificate: https://global-root-g2.chain-demos.digicert.com/info/index.html
+ sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
+
+ return sslContext;
+ }
+
+ /**
+ * Create an SSLContext instance with the provided public certificate and private key that also trusts the public
+ * certificates loaded in your device's trusted root certification authorities certificate store.
+ * @param publicKeyCertificateString the public key to use for x509 authentication. Does not need to include the
+ * Iot Hub trusted certificate as it will be added automatically as long as it is
+ * in your device's trusted root certification authorities certificate store.
+ * @param privateKeyString The private key to use for x509 authentication
+ * @return The created SSLContext that uses the provided public key and private key
+ * @throws GeneralSecurityException If the certificate creation fails, or if the SSLContext creation using those certificates fails.
+ * @throws IOException If the certificates cannot be read.
+ */
+ public static SSLContext buildSSLContext(String publicKeyCertificateString, String privateKeyString) throws GeneralSecurityException, IOException
+ {
+ Key privateKey = parsePrivateKeyString(privateKeyString);
+ Certificate[] publicKeyCertificates = parsePublicCertificateString(publicKeyCertificateString);
+
+ char[] temporaryPassword = generateTemporaryPassword();
+
+ KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+ keystore.load(null);
+ keystore.setCertificateEntry(CERTIFICATE_ALIAS, publicKeyCertificates[0]);
+ keystore.setKeyEntry(PRIVATE_KEY_ALIAS, privateKey, temporaryPassword, publicKeyCertificates);
+
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(keystore, temporaryPassword);
+
+ SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_INSTANCE);
+
+ // By leaving the TrustManager array null, the SSLContext will trust the certificates stored on your device's
+ // trusted root certification authorities certificate store.
+ sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
+
+ return sslContext;
+ }
+
+ private static PrivateKey parsePrivateKeyString(String privateKeyPEM) throws GeneralSecurityException
+ {
+ if (privateKeyPEM == null || privateKeyPEM.isEmpty())
+ {
+ throw new IllegalArgumentException("Private key cannot be null or empty");
+ }
+
+ if (privateKeyPEM.contains("BEGIN PRIVATE KEY"))
+ {
+ // If it is an RSA private key
+ privateKeyPEM = privateKeyPEM.replace("-----BEGIN PRIVATE KEY-----\r\n", "");
+ privateKeyPEM = privateKeyPEM.replace("-----END PRIVATE KEY-----\r\n", "");
+ byte[] encoded = Base64.decodeBase64(privateKeyPEM.getBytes(StandardCharsets.UTF_8));
+ KeyFactory kf = KeyFactory.getInstance(RSA_PRIVATE_KEY_ALGORITHM);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
+ return kf.generatePrivate(keySpec);
+ }
+ else if (privateKeyPEM.contains("BEGIN EC PRIVATE KEY"))
+ {
+ // If it is an ECC private key
+ privateKeyPEM = privateKeyPEM.replace("-----BEGIN EC PRIVATE KEY-----\r\n", "");
+ privateKeyPEM = privateKeyPEM.replace("-----END EC PRIVATE KEY-----\r\n", "");
+ byte[] encoded = Base64.decodeBase64(privateKeyPEM.getBytes(StandardCharsets.UTF_8));
+ KeyFactory kf = KeyFactory.getInstance(ECC_PRIVATE_KEY_ALGORITHM);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
+ return kf.generatePrivate(keySpec);
+ }
+ else
+ {
+ throw new IllegalArgumentException("Malformed private key");
+ }
+ }
+
+ private static X509Certificate[] parsePublicCertificateString(String pemString) throws GeneralSecurityException, IOException
+ {
+ if (pemString == null || pemString.isEmpty())
+ {
+ throw new IllegalArgumentException("Public key certificate cannot be null or empty");
+ }
+
+ try (InputStream pemInputStream = new ByteArrayInputStream(pemString.getBytes(StandardCharsets.UTF_8)))
+ {
+ CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE);
+ Collection collection = new ArrayList<>();
+ X509Certificate x509Cert;
+
+ while (pemInputStream.available() > 0)
+ {
+ x509Cert = (X509Certificate) cf.generateCertificate(pemInputStream);
+ collection.add(x509Cert);
+ }
+
+ return collection.toArray(new X509Certificate[0]);
+ }
+ }
+
+ private static char[] generateTemporaryPassword()
+ {
+ char[] randomChars = new char[256];
+ SecureRandom secureRandom = new SecureRandom();
+
+ for (int i = 0; i < 256; i++)
+ {
+ // character will be between 97 and 122 on the ASCII table. This forces it to be a lower case character.
+ // that ensures that the password, as a whole, is alphanumeric
+ randomChars[i] = (char) (97 + secureRandom.nextInt(26));
+ }
+
+ return randomChars;
+ }
+}
diff --git a/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/resources/log4j2.properties b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/resources/log4j2.properties
new file mode 100644
index 0000000000..4451929a10
--- /dev/null
+++ b/provisioning/provisioning-device-client-samples/certificate-signing-sample/src/main/resources/log4j2.properties
@@ -0,0 +1,13 @@
+status = error
+name = Log4j2PropertiesConfig
+
+appenders = console
+
+appender.console.type = Console
+appender.console.name = LogToConsole
+appender.console.layout.type = PatternLayout
+appender.console.layout.pattern = %d %p (%t) [%c] - %m%n
+
+rootLogger.level = debug
+rootLogger.appenderRefs = stdout
+rootLogger.appenderRef.stdout.ref = LogToConsole
\ No newline at end of file
diff --git a/provisioning/provisioning-device-client-samples/pom.xml b/provisioning/provisioning-device-client-samples/pom.xml
index fe3fb95517..38a4eaf51c 100644
--- a/provisioning/provisioning-device-client-samples/pom.xml
+++ b/provisioning/provisioning-device-client-samples/pom.xml
@@ -56,6 +56,7 @@
provisioning-symmetrickey-individual-sampleprovisioning-tpm-sampleprovisioning-X509-sample
+ certificate-signing-sample
diff --git a/provisioning/provisioning-device-client-samples/provisioning-X509-sample/pom.xml b/provisioning/provisioning-device-client-samples/provisioning-X509-sample/pom.xml
index 6db59ebea5..f7fb740ca7 100644
--- a/provisioning/provisioning-device-client-samples/provisioning-X509-sample/pom.xml
+++ b/provisioning/provisioning-device-client-samples/provisioning-X509-sample/pom.xml
@@ -36,11 +36,11 @@
org.bouncycastle
- bcmail-jdk15on
+ bcprov-jdk18onorg.bouncycastle
- bcprov-jdk15on
+ bcpkix-jdk18on
diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/AdditionalData.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/AdditionalData.java
index 4f0c06847c..352886db8a 100644
--- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/AdditionalData.java
+++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/AdditionalData.java
@@ -18,4 +18,18 @@ public class AdditionalData
@Getter
@Setter
private String provisioningPayload;
+
+ /**
+ *
+ * the base64-encoded Certificate Signing Request (CSR) to be sent during registration.
+ * When set, the DPS service will return an issued certificate chain in the registration result.
+ *
+ *
+ * The CSR should be a base64-encoded DER format CSR.
+ * The Common Name (CN) in the CSR should match the registration ID.
+ *
+ * the issued client certificate chain in response to a certificate signing request.
+ * This list will be null if no certificate signing request was provided during registration.
+ *
+ *
+ * The certificate chain is returned as an array of base64-encoded certificates.
+ * The first element is the device/leaf certificate, followed by intermediate CA certificates.
+ *
+ */
+ @Getter
+ protected List issuedClientCertificateChain;
}
diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/ProvisioningDeviceClientConfig.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/ProvisioningDeviceClientConfig.java
index 999ee2bf56..a0013a74d5 100644
--- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/ProvisioningDeviceClientConfig.java
+++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/ProvisioningDeviceClientConfig.java
@@ -50,6 +50,10 @@ public final class ProvisioningDeviceClientConfig
@Setter
private String payload;
+ @Getter
+ @Setter
+ private String certificateSigningRequest;
+
/**
* Setter for the Registration Callback.
* @param registrationCallback Registration Callback to be triggered.
diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/SDKUtils.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/SDKUtils.java
index 864618cc3a..2d21ad3cc9 100644
--- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/SDKUtils.java
+++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/SDKUtils.java
@@ -19,7 +19,7 @@
@Slf4j
public class SDKUtils
{
- private static final String SERVICE_API_VERSION = "2019-03-31";
+ private static final String SERVICE_API_VERSION = "2025-07-01-preview";
public static final String PROVISIONING_DEVICE_CLIENT_IDENTIFIER = "com.microsoft.azure.sdk.iot.dps.dps-device-client/";
public static final String PROVISIONING_DEVICE_CLIENT_VERSION = getPackageVersion();
diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/amqp/ContractAPIAmqp.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/amqp/ContractAPIAmqp.java
index b73fd48c23..67d642c0e9 100644
--- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/amqp/ContractAPIAmqp.java
+++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/amqp/ContractAPIAmqp.java
@@ -65,6 +65,11 @@ public ContractAPIAmqp(ProvisioningDeviceClientConfig provisioningDeviceClientCo
throw new ProvisioningDeviceClientException("The hostName cannot be null or empty.");
}
+ if (provisioningDeviceClientConfig.getCertificateSigningRequest() != null && !provisioningDeviceClientConfig.getCertificateSigningRequest().isEmpty())
+ {
+ throw new UnsupportedOperationException("Including a certificate signing request with a provisioning request is not supported over AMQP or AMQP_WS");
+ }
+
this.hostName = hostName;
this.useWebSockets = provisioningDeviceClientConfig.isUsingWebSocket();
diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/http/ContractAPIHttp.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/http/ContractAPIHttp.java
index 83d8bf3c54..82e7474120 100644
--- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/http/ContractAPIHttp.java
+++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/http/ContractAPIHttp.java
@@ -97,6 +97,11 @@ public ContractAPIHttp(ProvisioningDeviceClientConfig provisioningDeviceClientCo
//SRS_ContractAPIHttp_25_001: [The constructor shall save the scope id and hostname.]
this.idScope = idScope;
this.hostName = hostName;
+
+ if (provisioningDeviceClientConfig.getCertificateSigningRequest() != null && !provisioningDeviceClientConfig.getCertificateSigningRequest().isEmpty())
+ {
+ throw new UnsupportedOperationException("Including a certificate signing request with a provisioning request is not supported over HTTP");
+ }
}
private HttpRequest prepareRequest(
diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/mqtt/ContractAPIMqtt.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/mqtt/ContractAPIMqtt.java
index 2e899f8900..802b1dad36 100644
--- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/mqtt/ContractAPIMqtt.java
+++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/mqtt/ContractAPIMqtt.java
@@ -66,7 +66,6 @@ public String getHostName() {
*/
public ContractAPIMqtt(ProvisioningDeviceClientConfig provisioningDeviceClientConfig) throws ProvisioningDeviceClientException
{
- // SRS_ContractAPIMqtt_07_024: [ If provisioningDeviceClientConfig is null, this method shall throw ProvisioningDeviceClientException. ]
if (provisioningDeviceClientConfig == null)
{
throw new ProvisioningDeviceClientException("ProvisioningDeviceClientConfig cannot be NULL.");
@@ -95,7 +94,6 @@ private void executeProvisioningMessage(String topic, byte[] body, ResponseCallb
try
{
- // SRS_ProvisioningAmqpOperations_07_011: [This method shall wait for the response of this message for MAX_WAIT_TO_SEND_MSG and call the responseCallback with the reply.]
synchronized (this.receiveLock)
{
this.receiveLock.waitLock(MAX_WAIT_TO_SEND_MSG);
@@ -113,7 +111,6 @@ private void executeProvisioningMessage(String topic, byte[] body, ResponseCallb
}
catch (InterruptedException e)
{
- // SRS_ProvisioningAmqpOperations_07_012: [This method shall throw ProvisioningDeviceClientException if any failure is encountered.]
throw new ProvisioningDeviceClientException("Provisioning service failed to reply is allotted time.");
}
@@ -225,7 +222,6 @@ public synchronized void close() throws ProvisioningDeviceConnectionException
*/
public synchronized void authenticateWithProvisioningService(RequestData requestData, ResponseCallback responseCallback, Object callbackContext) throws ProvisioningDeviceClientException
{
- //SRS_ContractAPIAmqp_07_003: [If responseCallback is null, this method shall throw ProvisioningDeviceClientException.]
if (responseCallback == null)
{
throw new ProvisioningDeviceClientException("responseCallback cannot be null");
@@ -236,13 +232,9 @@ public synchronized void authenticateWithProvisioningService(RequestData request
String sasToken = requestData.getSasToken();
if (sasToken == null || sasToken.isEmpty())
{
- //SRS_ContractAPIAmqp_34_021: [If the requestData is not x509, but the provided requestData does not contain a sas token, this function shall
- // throw a ProvisioningDeviceConnectionException.]
throw new ProvisioningDeviceConnectionException(new IllegalArgumentException("RequestData's sas token cannot be null or empty"));
}
- //SRS_ContractAPIAmqp_34_020: [If the requestData is not x509, this function shall assume SymmetricKey authentication, and shall open the connection with
- // the provided request data containing a sas token.]
open(requestData);
}
@@ -255,10 +247,8 @@ public synchronized void authenticateWithProvisioningService(RequestData request
{
String topic = String.format(MQTT_REGISTER_MESSAGE_FMT, this.packetId++);
- //SRS_ContractAPIMqtt_07_026: [ This method shall build the required Json input using parser. ]
- byte[] payload = new DeviceRegistrationParser(requestData.getRegistrationId(), requestData.getPayload()).toJson().getBytes(StandardCharsets.UTF_8);
+ byte[] payload = new DeviceRegistrationParser(requestData.getRegistrationId(), requestData.getPayload(), requestData.getCertificateSigningRequest()).toJson().getBytes(StandardCharsets.UTF_8);
- // SRS_ContractAPIMqtt_07_005: [This method shall send an MQTT message with the property of iotdps-register.]
this.executeProvisioningMessage(topic, payload, responseCallback, callbackContext);
}
catch (IOException ex)
diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationParser.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationParser.java
index af6855614c..1cac6860ba 100644
--- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationParser.java
+++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationParser.java
@@ -28,6 +28,9 @@ public class DeviceRegistrationParser
@SerializedName(CUSTOM_PAYLOAD)
private String customPayload = null;
+ @SerializedName("csr")
+ private String certificateSigningRequest = null;
+
/**
* Inner class describing TPM Attestation i.e it holds EndorsementKey and StorageRootKey
*/
@@ -58,18 +61,23 @@ static class TpmAttestation
*/
public DeviceRegistrationParser(String registrationId, String customPayload) throws IllegalArgumentException
{
- //SRS_DeviceRegistration_25_001: [ The constructor shall throw IllegalArgumentException if Registration Id is null or empty. ]
+ this(registrationId, customPayload, null);
+ }
+
+ public DeviceRegistrationParser(String registrationId, String customPayload, String certificateSigningRequest) throws IllegalArgumentException
+ {
if (registrationId == null || registrationId.isEmpty())
{
throw new IllegalArgumentException("Registration Id cannot be null or empty");
}
- //SRS_DeviceRegistration_25_002: [ The constructor shall save the provided Registration Id. ]
this.registrationId = registrationId;
if (customPayload != null && !customPayload.isEmpty())
{
this.customPayload = customPayload;
}
+
+ this.certificateSigningRequest = certificateSigningRequest;
}
/**
@@ -82,19 +90,22 @@ public DeviceRegistrationParser(String registrationId, String customPayload) thr
*/
public DeviceRegistrationParser(String registrationId, String customPayload, String endorsementKey, String storageRootKey) throws IllegalArgumentException
{
- //SRS_DeviceRegistration_25_003: [ The constructor shall throw IllegalArgumentException if Registration Id is null or empty. ]
+ this(registrationId, customPayload, null, endorsementKey, storageRootKey);
+ }
+
+ public DeviceRegistrationParser(String registrationId, String customPayload, String certificateSigningRequest, String endorsementKey, String storageRootKey) throws IllegalArgumentException
+ {
if (registrationId == null || registrationId.isEmpty())
{
throw new IllegalArgumentException("Registration Id cannot be null or empty");
}
- //SRS_DeviceRegistration_25_004: [ The constructor shall throw IllegalArgumentException if EndorsementKey is null or empty. ]
if (endorsementKey == null || endorsementKey.isEmpty())
{
throw new IllegalArgumentException("endorsementKey cannot be null or empty");
}
- //SRS_DeviceRegistration_25_006: [ The constructor shall save the provided Registration Id, EndorsementKey and StorageRootKey. ]
+ this.certificateSigningRequest = certificateSigningRequest;
this.registrationId = registrationId;
if (customPayload != null && !customPayload.isEmpty())
{
@@ -107,22 +118,22 @@ public DeviceRegistrationParser(String registrationId, String customPayload, Str
* Generates JSON output for this class.
* Expected format :
* For TPM :
- *