diff --git a/e2e/test/config/TestConfiguration.Provisioning.cs b/e2e/test/config/TestConfiguration.Provisioning.cs index a65f27e557..80d8727de7 100644 --- a/e2e/test/config/TestConfiguration.Provisioning.cs +++ b/e2e/test/config/TestConfiguration.Provisioning.cs @@ -1,8 +1,19 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Globalization; +using System.Net; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Text; +using Microsoft.Azure.Devices.E2ETests.Provisioning; + +#if !NET451 + +using Azure.Identity; + +#endif namespace Microsoft.Azure.Devices.E2ETests { @@ -14,6 +25,34 @@ public static partial class Provisioning public static string ConnectionString => GetValue("PROVISIONING_CONNECTION_STRING"); + public static string GetProvisioningHostName() + { + var connectionString = new ConnectionStringParser(ConnectionString); + return connectionString.ProvisioningHostName; + } + +#if !NET451 + + public static ClientSecretCredential GetClientSecretCredential() + { + return new ClientSecretCredential( + GetValue("MSFT_TENANT_ID"), + GetValue("IOTHUB_CLIENT_ID"), + GetValue("IOTHUB_CLIENT_SECRET")); + } + +#endif + + public static string GetProvisioningSharedAccessSignature(TimeSpan timeToLive) + { + var connectionString = new ConnectionStringParser(ConnectionString); + return GenerateSasToken( + connectionString.ProvisioningHostName, + connectionString.SharedAccessKey, + timeToLive, + connectionString.SharedAccessKeyName); + } + public static string GlobalDeviceEndpoint => GetValue("DPS_GLOBALDEVICEENDPOINT", "global.azure-devices-provisioning.net"); @@ -38,6 +77,35 @@ public static X509Certificate2Collection GetGroupEnrollmentChain() public static string FarAwayIotHubHostName => GetValue("FAR_AWAY_IOTHUB_HOSTNAME"); public static string CustomAllocationPolicyWebhook => GetValue("CUSTOM_ALLOCATION_POLICY_WEBHOOK"); + + private static string GenerateSasToken(string resourceUri, string sharedAccessKey, TimeSpan timeToLive, string policyName = default) + { + var epochTime = new DateTime(1970, 1, 1); + DateTime expiresOn = DateTime.UtcNow.Add(timeToLive); + TimeSpan secondsFromEpochTime = expiresOn.Subtract(epochTime); + long seconds = Convert.ToInt64(secondsFromEpochTime.TotalSeconds, CultureInfo.InvariantCulture); + string expiry = Convert.ToString(seconds, CultureInfo.InvariantCulture); + + string stringToSign = WebUtility.UrlEncode(resourceUri) + "\n" + expiry; + + using var hmac = new HMACSHA256(Convert.FromBase64String(sharedAccessKey)); + string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign))); + + // SharedAccessSignature sr=ENCODED(dh://myiothub.azure-devices.net/a/b/c?myvalue1=a)&sig=&se=[&skn=] + string token = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}&se={2}", + WebUtility.UrlEncode(resourceUri), + WebUtility.UrlEncode(signature), + expiry); + + if (!string.IsNullOrWhiteSpace(policyName)) + { + token += "&skn=" + policyName; + } + + return token; + } } } } diff --git a/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 b/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 index 0ac2a2e319..6c1c3c0909 100644 --- a/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 +++ b/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 @@ -610,13 +610,14 @@ if ($certExists) az iot dps certificate delete -g $ResourceGroup --dps-name $dpsName --name $uploadCertificateName --etag $etag } Write-Host "`nUploading new certificate to DPS." -az iot dps certificate create -g $ResourceGroup --path $rootCertPath --dps-name $dpsName --certificate-name $uploadCertificateName --output none +az iot dps certificate create -g $ResourceGroup --path $rootCertPath --dps-name $dpsName --certificate-name $uploadCertificateName $isVerified = az iot dps certificate show -g $ResourceGroup --dps-name $dpsName --certificate-name $uploadCertificateName --query 'properties.isVerified' --output tsv if ($isVerified -eq 'false') { Write-Host "`nVerifying certificate uploaded to DPS." $etag = az iot dps certificate show -g $ResourceGroup --dps-name $dpsName --certificate-name $uploadCertificateName --query 'etag' + Write-Host "`n az iot dps certificate generate-verification-code -g $ResourceGroup --dps-name $dpsName --certificate-name $uploadCertificateName -e $etag --query 'properties.verificationCode'" $requestedCommonName = az iot dps certificate generate-verification-code -g $ResourceGroup --dps-name $dpsName --certificate-name $uploadCertificateName -e $etag --query 'properties.verificationCode' $verificationCertArgs = @{ "-DnsName" = $requestedCommonName; diff --git a/e2e/test/provisioning/ConnectionStringParser.cs b/e2e/test/provisioning/ConnectionStringParser.cs new file mode 100644 index 0000000000..958c78ebb2 --- /dev/null +++ b/e2e/test/provisioning/ConnectionStringParser.cs @@ -0,0 +1,59 @@ +using System; + +namespace Microsoft.Azure.Devices.E2ETests.Provisioning +{ + internal class ConnectionStringParser + { + public string ProvisioningHostName { get; private set; } + + public string DeviceId { get; private set; } + + public string SharedAccessKey { get; private set; } + + public string SharedAccessKeyName { get; private set; } + + public ConnectionStringParser(string connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new ArgumentException(nameof(connectionString), "Parameter cannot be null, empty, or whitespace."); + } + + string[] parts = connectionString.Split(';'); + + foreach (string part in parts) + { + int separatorIndex = part.IndexOf('='); + if (separatorIndex < 0) + { + throw new ArgumentException($"Improperly formatted key/value pair: {part}."); + } + + string key = part.Substring(0, separatorIndex); + string value = part.Substring(separatorIndex + 1); + + switch (key.ToUpperInvariant()) + { + case "HOSTNAME": + ProvisioningHostName = value; + break; + + case "SHAREDACCESSKEY": + SharedAccessKey = value; + break; + + case "DEVICEID": + DeviceId = value; + break; + + case "SHAREDACCESSKEYNAME": + SharedAccessKeyName = value; + break; + + default: + throw new NotSupportedException($"Unrecognized tag found in parameter {nameof(connectionString)}."); + } + } + } + } +} diff --git a/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs b/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs index 1ada9be022..e847566278 100644 --- a/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs +++ b/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Net; using System.Threading.Tasks; +using Azure; +using FluentAssertions; using Microsoft.Azure.Devices.Provisioning.Security.Samples; using Microsoft.Azure.Devices.Provisioning.Service; using Microsoft.Azure.Devices.Shared; @@ -110,6 +112,63 @@ public async Task ProvisioningServiceClient_GetEnrollmentGroupAttestation_Symmet await ProvisioningServiceClient_GetEnrollmentGroupAttestation(AttestationMechanismType.SymmetricKey); } + [LoggedTestMethod] + public async Task ProvisioningServiceClient_TokenCredentialAuth_Success() + { + // arrange + using var provisioningServiceClient = ProvisioningServiceClient.Create( + TestConfiguration.Provisioning.GetProvisioningHostName(), + TestConfiguration.Provisioning.GetClientSecretCredential()); + + IndividualEnrollment individualEnrollment = await CreateIndividualEnrollment( + provisioningServiceClient, + AttestationMechanismType.SymmetricKey, + null, + AllocationPolicy.Static, + null, + null, + null) + .ConfigureAwait(false); + + // act + IndividualEnrollment individualEnrollmentResult = await provisioningServiceClient.CreateOrUpdateIndividualEnrollmentAsync(individualEnrollment); + + // assert + individualEnrollmentResult.Should().NotBeNull(); + + // cleanup + await provisioningServiceClient.DeleteIndividualEnrollmentAsync(individualEnrollment.RegistrationId); + } + + [LoggedTestMethod] + public async Task ProvisioningServiceClient_AzureSasCredentialAuth_Success() + { + // arrange + string signature = TestConfiguration.Provisioning.GetProvisioningSharedAccessSignature(TimeSpan.FromHours(1)); + using var provisioningServiceClient = ProvisioningServiceClient.Create( + TestConfiguration.Provisioning.GetProvisioningHostName(), + new AzureSasCredential(signature)); + + IndividualEnrollment individualEnrollment = await CreateIndividualEnrollment( + provisioningServiceClient, + AttestationMechanismType.SymmetricKey, + null, + AllocationPolicy.Static, + null, + null, + null) + .ConfigureAwait(false); + + // act + IndividualEnrollment individualEnrollmentResult = await provisioningServiceClient.CreateOrUpdateIndividualEnrollmentAsync(individualEnrollment); + + // assert + individualEnrollmentResult.Should().NotBeNull(); + + // cleanup + await provisioningServiceClient.DeleteIndividualEnrollmentAsync(individualEnrollment.RegistrationId); + } + public async Task ProvisioningServiceClient_GetIndividualEnrollmentAttestation(AttestationMechanismType attestationType) { using var provisioningServiceClient = ProvisioningServiceClient.CreateFromConnectionString(TestConfiguration.Provisioning.ConnectionString); diff --git a/provisioning/service/src/Contract/SDKUtils.cs b/provisioning/service/src/Contract/SDKUtils.cs index c21488deaf..82a2b31a4e 100644 --- a/provisioning/service/src/Contract/SDKUtils.cs +++ b/provisioning/service/src/Contract/SDKUtils.cs @@ -5,7 +5,7 @@ namespace Microsoft.Azure.Devices.Provisioning.Service { internal class SDKUtils { - private const string ApiVersionProvisioning = "2019-03-31"; + private const string ApiVersionProvisioning = "2021-10-01"; public const string ApiVersionQueryString = CustomHeaderConstants.ApiVersion + "=" + ApiVersionProvisioning; } }