diff --git a/iot-e2e-tests/common/pom.xml b/iot-e2e-tests/common/pom.xml index 57bee43436..c49f3d07e6 100644 --- a/iot-e2e-tests/common/pom.xml +++ b/iot-e2e-tests/common/pom.xml @@ -71,13 +71,7 @@ org.bouncycastle - bcmail-jdk15on - 1.70 - - - org.bouncycastle - bcprov-jdk15on - 1.70 + bcprov-jdk18on org.apache.logging.log4j diff --git a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/DeviceClient.java b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/DeviceClient.java index 102f7e5244..b4ae507fa4 100644 --- a/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/DeviceClient.java +++ b/iothub/device/iot-device-client/src/main/java/com/microsoft/azure/sdk/iot/device/DeviceClient.java @@ -3,14 +3,18 @@ package 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.device.transport.IotHubTransportMessage; import com.microsoft.azure.sdk.iot.device.transport.RetryPolicy; import com.microsoft.azure.sdk.iot.device.transport.TransportUtils; import com.microsoft.azure.sdk.iot.device.transport.https.HttpsTransportManager; +import com.microsoft.azure.sdk.iot.device.twin.DeviceOperations; import com.microsoft.azure.sdk.iot.provisioning.security.SecurityProvider; import lombok.extern.slf4j.Slf4j; import java.io.IOException; +import java.util.concurrent.CompletableFuture; /** *

@@ -271,6 +275,95 @@ public boolean isMultiplexed() return this.isMultiplexed; } + /** + *

+ * 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}. + *

+ */ + @Getter + @Setter + Future OnCertificateSigningRequestAccepted; + + /** + *

+ * 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.core jackson-core - 2.21.0 + 2.21.1 com.fasterxml.jackson.core @@ -147,13 +147,18 @@ org.bouncycastle - bcmail-jdk15on - 1.70 + bcprov-jdk18on + 1.83 + + + org.bouncycastle + bcpkix-jdk18on + 1.83 org.bouncycastle bcprov-jdk15on - 1.70 + 1.70 org.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-sample provisioning-tpm-sample provisioning-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-jdk18on org.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. + *

+ */ + @Getter + @Setter + private String clientCertificateSigningRequest; } diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/ProvisioningDeviceClient.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/ProvisioningDeviceClient.java index e18b12fc8f..ff0ac8317d 100644 --- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/ProvisioningDeviceClient.java +++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/ProvisioningDeviceClient.java @@ -117,6 +117,7 @@ public void registerDevice(ProvisioningDeviceClientRegistrationCallback provisio if (additionalData != null) { this.provisioningDeviceClientConfig.setPayload(additionalData.getProvisioningPayload()); + this.provisioningDeviceClientConfig.setCertificateSigningRequest(additionalData.getClientCertificateSigningRequest()); } this.provisioningDeviceClientConfig.setRegistrationCallback(provisioningDeviceClientRegistrationCallback, context); diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/ProvisioningDeviceClientRegistrationResult.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/ProvisioningDeviceClientRegistrationResult.java index 2010a672bd..f77ec60fdb 100644 --- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/ProvisioningDeviceClientRegistrationResult.java +++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/ProvisioningDeviceClientRegistrationResult.java @@ -10,6 +10,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.Collection; +import java.util.List; + @NoArgsConstructor public class ProvisioningDeviceClientRegistrationResult { @@ -42,4 +45,17 @@ public class ProvisioningDeviceClientRegistrationResult @Getter protected String provisioningPayload; + + /** + *

+ * 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 : - *
+     * 
      *     {@code
      *     "{\"registrationId\":\"[RegistrationID value]\"," +
-            "\"tpm\":{" +
-            "\"endorsementKey\":\"[Endorsement Key value]\"," +
-            "\"storageRootKey\":\"[Storage root key value]\"" +
-            "}
-            "\"payload\":\"[Custom Data]\""
-            }"
+    "\"tpm\":{" +
+    "\"endorsementKey\":\"[Endorsement Key value]\"," +
+    "\"storageRootKey\":\"[Storage root key value]\"" +
+    "}
+    "\"payload\":\"[Custom Data]\""
+    }"
      *     }
-         * 
+ *
* For X509: *
      *     {@code
      *     "{\"registrationId\":\"[RegistrationID value]\"," +
-            }"
+    }"
      *     }
      * 
* @return A string that is JSON formatted. @@ -133,4 +144,4 @@ public String toJson() Gson gson = new GsonBuilder().disableHtmlEscaping().create(); return gson.toJson(this); } -} +} \ No newline at end of file diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationResultParser.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationResultParser.java index c907dd9b76..ba90e10cc7 100644 --- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationResultParser.java +++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationResultParser.java @@ -11,6 +11,8 @@ import com.google.gson.annotations.SerializedName; import lombok.Getter; +import java.util.List; + /** * Class that represents the REST API format for DeviceRegistrationResult * Format : https://docs.microsoft.com/en-us/rest/api/iot-dps/RuntimeRegistration/RegisterDevice#definitions_deviceregistrationresult @@ -18,71 +20,62 @@ @SuppressWarnings("unused") // A number of private fields are unused but may be filled in by serialization public class DeviceRegistrationResultParser { - private static final String REGISTRATION_ID = "registrationId"; - @SerializedName(REGISTRATION_ID) + @SerializedName("registrationId") @Getter private String registrationId; - private static final String CREATED_DATE_TIME_UTC = "createdDateTimeUtc"; - @SerializedName(CREATED_DATE_TIME_UTC) + @SerializedName("createdDateTimeUtc") @Getter private String createdDateTimeUtc; - private static final String ASSIGNED_HUB = "assignedHub"; - @SerializedName(ASSIGNED_HUB) + @SerializedName("assignedHub") @Getter private String assignedHub; - private static final String DEVICE_ID = "deviceId"; - @SerializedName(DEVICE_ID) + @SerializedName("deviceId") @Getter private String deviceId; - private static final String STATUS = "status"; - @SerializedName(STATUS) + @SerializedName("status") @Getter private String status; - private static final String SUBSTATUS = "substatus"; - @SerializedName(SUBSTATUS) + @SerializedName("substatus") @Getter private String substatus; - private static final String ETAG = "etag"; - @SerializedName(ETAG) + @SerializedName("etag") @Getter private String eTag; - private static final String LAST_UPDATES_DATE_TIME_UTC = "lastUpdatedDateTimeUtc"; - @SerializedName(LAST_UPDATES_DATE_TIME_UTC) + @SerializedName("lastUpdatedDateTimeUtc") @Getter private String lastUpdatesDateTimeUtc; - private static final String ERROR_CODE = "errorCode"; - @SerializedName(ERROR_CODE) + @SerializedName("errorCode") @Getter private Integer errorCode; - private static final String ERROR_MESSAGE = "errorMessage"; - @SerializedName(ERROR_MESSAGE) + @SerializedName("errorMessage") @Getter private String errorMessage; - private static final String TPM = "tpm"; - @SerializedName(TPM) + @SerializedName("tpm") @Getter private TpmRegistrationResultParser tpm; - private static final String X509 = "x509"; - @SerializedName(X509) + @SerializedName("x509") @Getter private X509RegistrationResultParser x509; - private static final String PAYLOAD = "payload"; - @SerializedName(PAYLOAD) + @SerializedName("payload") @Getter private JsonObject jsonPayload; + @SerializedName("issuedCertificateChain") + @Getter + private List issuedCertificateChain; + //empty constructor for Gson DeviceRegistrationResultParser() { diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/ProvisioningTask.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/ProvisioningTask.java index e301bcaf8c..73859b1f65 100644 --- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/ProvisioningTask.java +++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/ProvisioningTask.java @@ -224,6 +224,7 @@ private void executeStateMachineForStatus(RegistrationOperationStatusParser regi registrationInfo.setCreatedDateTimeUtc(registrationStatus.getCreatedDateTimeUtc()); registrationInfo.setLastUpdatesDateTimeUtc(registrationStatus.getLastUpdatesDateTimeUtc()); registrationInfo.setETag(registrationStatus.getETag()); + registrationInfo.setIssuedCertificateChain(registrationStatus.getIssuedCertificateChain()); if (this.securityProvider instanceof SecurityProviderTpm) { @@ -300,7 +301,7 @@ public Object call() throws Exception { //SRS_ProvisioningTask_25_015: [ This method shall invoke open call on the contract.] log.info("Opening the connection to device provisioning service..."); - provisioningDeviceClientContract.open(new RequestData(securityProvider.getRegistrationId(), securityProvider.getSSLContext(), securityProvider instanceof SecurityProviderX509, provisioningDeviceClientConfig.getPayload())); + provisioningDeviceClientContract.open(new RequestData(securityProvider.getRegistrationId(), securityProvider.getSSLContext(), securityProvider instanceof SecurityProviderX509, provisioningDeviceClientConfig.getPayload(), provisioningDeviceClientConfig.getCertificateSigningRequest())); //SRS_ProvisioningTask_25_007: [ This method shall invoke Register task and status task to execute the state machine of the service as per below rules.] /* Service State Machine Rules diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RegisterTask.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RegisterTask.java index ea638659c6..bd96fb42fe 100644 --- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RegisterTask.java +++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RegisterTask.java @@ -294,7 +294,7 @@ private RegistrationOperationStatusParser authenticateWithDPS() throws Provision if (this.securityProvider instanceof SecurityProviderX509) { - RequestData requestData = new RequestData(securityProvider.getRegistrationId(), sslContext, true, this.provisioningDeviceClientConfig.getPayload()); + RequestData requestData = new RequestData(securityProvider.getRegistrationId(), sslContext, true, this.provisioningDeviceClientConfig.getPayload(), this.provisioningDeviceClientConfig.getCertificateSigningRequest()); log.info("Authenticating with device provisioning service using x509 certificates"); return this.authenticateWithX509(requestData); } @@ -314,7 +314,7 @@ else if (this.securityProvider instanceof SecurityProviderTpm) } else if (this.securityProvider instanceof SecurityProviderSymmetricKey) { - RequestData requestData = new RequestData(securityProvider.getRegistrationId(), sslContext, null, this.provisioningDeviceClientConfig.getPayload()); + RequestData requestData = new RequestData(securityProvider.getRegistrationId(), sslContext, null, this.provisioningDeviceClientConfig.getPayload(), this.provisioningDeviceClientConfig.getCertificateSigningRequest()); log.info("Authenticating with device provisioning service using symmetric key"); return this.authenticateWithSasToken(requestData); diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RegistrationResult.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RegistrationResult.java index 17b85a1d43..4045f66055 100644 --- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RegistrationResult.java +++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RegistrationResult.java @@ -11,6 +11,8 @@ import com.microsoft.azure.sdk.iot.provisioning.device.ProvisioningDeviceClientStatus; import com.microsoft.azure.sdk.iot.provisioning.device.ProvisioningDeviceClientSubstatus; +import java.util.List; + class RegistrationResult extends ProvisioningDeviceClientRegistrationResult { /** @@ -58,4 +60,9 @@ void setLastUpdatesDateTimeUtc(String lastUpdatesDateTimeUtc) { this.lastUpdatesDateTimeUtc = lastUpdatesDateTimeUtc; } + + void setIssuedCertificateChain(List issuedCertificateChain) + { + this.issuedClientCertificateChain = issuedCertificateChain; + } } diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RequestData.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RequestData.java index d250994162..350db1ea13 100644 --- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RequestData.java +++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/RequestData.java @@ -45,6 +45,10 @@ public class RequestData @Setter(AccessLevel.PACKAGE) private String payload; + @Getter + @Setter(AccessLevel.PACKAGE) + private String certificateSigningRequest; + /** * Constructor for Request data * @param endorsementKey Endorsement key value. Can be {@code null} @@ -56,7 +60,6 @@ public class RequestData */ RequestData(byte[] endorsementKey, byte[] storageRootKey, String registrationId, SSLContext sslContext, String sasToken, String payload) { - //SRS_RequestData_25_001: [ Constructor shall save all the parameters and ignore the null parameters. ] this.endorsementKey = endorsementKey; this.storageRootKey = storageRootKey; this.registrationId = registrationId; @@ -75,12 +78,25 @@ public class RequestData */ RequestData(String registrationId, SSLContext sslContext, String sasToken, String payload) { - //SRS_RequestData_25_001: [ Constructor shall save all the parameters and ignore the null parameters. ] + this(registrationId, sslContext, sasToken, payload, null); + } + + /** + * Constructor for Request data + * @param registrationId Registration ID value. Can be {@code null} + * @param sslContext SSL context value. Can be {@code null} + * @param sasToken SasToken value. Can be {@code null} + * @param payload Payload value. Can be {@code null} + * @param certificateSigningRequest The optional certificate signing request. May be null or empty. + */ + RequestData(String registrationId, SSLContext sslContext, String sasToken, String payload, String certificateSigningRequest) + { this.registrationId = registrationId; this.sslContext = sslContext; this.sasToken = sasToken; this.payload = payload; this.isX509 = false; + this.certificateSigningRequest = certificateSigningRequest; } /** @@ -92,11 +108,24 @@ public class RequestData */ RequestData(String registrationId, SSLContext sslContext, boolean isX509, String payload) { - //SRS_RequestData_25_001: [ Constructor shall save all the parameters and ignore the null parameters. ] + this(registrationId, sslContext, isX509, payload, null); + } + + /** + * Constructor for Request data + * @param registrationId Registration ID value. Can be {@code null} + * @param sslContext SSL context value. Can be {@code null} + * @param isX509 True if X509 flow, false otherwise + * @param payload Payload value. Can be {@code null} + * @param certificateSigningRequest The optional certificate signing request. May be null or empty. + */ + RequestData(String registrationId, SSLContext sslContext, boolean isX509, String payload, String certificateSigningRequest) + { this.registrationId = registrationId; this.sslContext = sslContext; this.isX509 = isX509; this.payload = payload; + this.certificateSigningRequest = certificateSigningRequest; } /** @@ -109,7 +138,6 @@ public class RequestData */ RequestData(String registrationId, String operationId, SSLContext sslContext, String sasToken, String payload) { - //SRS_RequestData_25_001: [ Constructor shall save all the parameters and ignore the null parameters. ] this.registrationId = registrationId; this.operationId = operationId; this.sslContext = sslContext; @@ -124,7 +152,6 @@ public class RequestData */ public boolean isX509() { - //SRS_RequestData_25_015: [ This method shall return true is it is X509, false otherwise. ] return isX509; } } diff --git a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/StatusTask.java b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/StatusTask.java index 38f6e4ef06..ba045a29eb 100644 --- a/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/StatusTask.java +++ b/provisioning/provisioning-device-client/src/main/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/task/StatusTask.java @@ -66,7 +66,6 @@ public void run(ResponseData responseData, Object context) throws ProvisioningDe String operationId, Authorization authorization) throws ProvisioningDeviceClientException { - //SRS_StatusTask_25_002: [ Constructor shall throw ProvisioningDeviceClientException if operationId , securityProvider, authorization or provisioningDeviceClientContract is null. ] if (provisioningDeviceClientContract == null) { throw new ProvisioningDeviceClientException(new IllegalArgumentException("provisioningDeviceClientContract cannot be null")); @@ -92,7 +91,6 @@ public void run(ResponseData responseData, Object context) throws ProvisioningDe throw new ProvisioningDeviceClientException(new IllegalArgumentException("authorization cannot be null")); } - //SRS_StatusTask_25_001: [ Constructor shall save operationId , securityProvider, provisioningDeviceClientContract and authorization. ] this.securityProvider = securityProvider; this.provisioningDeviceClientContract = provisioningDeviceClientContract; this.provisioningDeviceClientConfig = provisioningDeviceClientConfig; @@ -104,14 +102,12 @@ private RegistrationOperationStatusParser getRegistrationStatus(String operation { try { - //SRS_StatusTask_25_003: [ This method shall throw ProvisioningDeviceClientException if registration id is null or empty. ] String registrationId = this.securityProvider.getRegistrationId(); if (registrationId == null || registrationId.isEmpty()) { throw new ProvisioningDeviceSecurityException("registrationId cannot be null or empty"); } - //SRS_StatusTask_25_004: [ This method shall retrieve the SSL context from Authorization and throw ProvisioningDeviceClientException if it is null. ] SSLContext sslContext = authorization.getSslContext(); if (sslContext == null) { @@ -119,7 +115,6 @@ private RegistrationOperationStatusParser getRegistrationStatus(String operation } RequestData requestData = new RequestData( registrationId, operationId, authorization.getSslContext(), authorization.getSasToken(), null); - //SRS_StatusTask_25_005: [ This method shall trigger getRegistrationState on the contract API and wait for response and return it. ] ResponseData responseData = new ResponseData(); provisioningDeviceClientContract.getRegistrationStatus(requestData, new ResponseCallbackImpl(), responseData); if (responseData.getResponseData() == null || responseData.getContractState() != ContractState.DPS_REGISTRATION_RECEIVED) diff --git a/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/UrlPathBuilderTest.java b/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/UrlPathBuilderTest.java index 0a7abcf887..e05b55f135 100644 --- a/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/UrlPathBuilderTest.java +++ b/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/UrlPathBuilderTest.java @@ -27,7 +27,7 @@ public class UrlPathBuilderTest private static final String TEST_HOST_NAME = "testHostName"; private static final String TEST_REGISTRATION_ID = "testRegistrationId"; private static final String TEST_OPERATION_ID = "testOperationId"; - private static final String SERVICE_API_VERSION = "2019-03-31"; + private static final String SERVICE_API_VERSION = "2025-07-01-preview"; //SRS_UrlPathBuilder_25_001: [ Constructor shall save scope id.] @Test diff --git a/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/mqtt/ContractAPIMqttTest.java b/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/mqtt/ContractAPIMqttTest.java index e8498ea2d9..e89614ce32 100644 --- a/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/mqtt/ContractAPIMqttTest.java +++ b/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/contract/mqtt/ContractAPIMqttTest.java @@ -433,7 +433,7 @@ public void authenticateWithProvisioningServiceWithX509Succeeds() throws Provisi mockedMqttConnection.isMqttConnected(); result = true; - Deencapsulation.newInstance(DeviceRegistrationParser.class, new Class[] {String.class, String.class}, TEST_REGISTRATION_ID, null); + Deencapsulation.newInstance(DeviceRegistrationParser.class, new Class[] {String.class, String.class, String.class}, TEST_REGISTRATION_ID, null, null); result = mockedDeviceRegistrationParser; mockedDeviceRegistrationParser.toJson(); @@ -503,7 +503,7 @@ public void authenticateWithProvisioningServiceWithPayloadSucceeds() throws Prov mockedRequestData.getPayload(); result = TEST_PAYLOAD; - Deencapsulation.newInstance(DeviceRegistrationParser.class, new Class[] {String.class, String.class}, TEST_REGISTRATION_ID, TEST_PAYLOAD); + Deencapsulation.newInstance(DeviceRegistrationParser.class, new Class[] {String.class, String.class, String.class}, TEST_REGISTRATION_ID, TEST_PAYLOAD, null); result = mockedDeviceRegistrationParser; mockedDeviceRegistrationParser.toJson(); @@ -600,7 +600,7 @@ public void authenticateWithProvisioningServiceSuccessWithSymmetricKey() throws mockedRequestData.getSslContext(); result = mockedSslContext; - Deencapsulation.newInstance(DeviceRegistrationParser.class, new Class[] {String.class, String.class}, TEST_REGISTRATION_ID, null); + Deencapsulation.newInstance(DeviceRegistrationParser.class, new Class[] {String.class, String.class, String.class}, TEST_REGISTRATION_ID, null, null); result = mockedDeviceRegistrationParser; mockedDeviceRegistrationParser.toJson(); diff --git a/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationParserTest.java b/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationParserTest.java index e50f3c1ed9..4c60b63797 100644 --- a/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationParserTest.java +++ b/provisioning/provisioning-device-client/src/test/java/com/microsoft/azure/sdk/iot/provisioning/device/internal/parser/DeviceRegistrationParserTest.java @@ -57,8 +57,8 @@ public void constructorWithTPMSucceed() throws Exception final String sRKey = "testStorageRootKey"; final String expectedJson = "{\"registrationId\":\"testID\"," + "\"tpm\":{" + - "\"endorsementKey\":\"testEndorsementKey\"," + - "\"storageRootKey\":\"testStorageRootKey\"" + + "\"endorsementKey\":\"testEndorsementKey\"," + + "\"storageRootKey\":\"testStorageRootKey\"" + "}}"; DeviceRegistrationParser deviceRegistrationParser = new DeviceRegistrationParser(regID, "", eKey, sRKey); @@ -99,4 +99,4 @@ public void constructorWithTPMOnEmptyEkThrows() throws Exception final String sRKey = "testStorageRootKey"; DeviceRegistrationParser deviceRegistrationParser = new DeviceRegistrationParser(TEST_REGISTRATION_ID, "", "", sRKey); } -} +} \ No newline at end of file