Exercício – configurar o dispositivo simulado com o certificado X.509
Neste exercício, você gera um certificado de dispositivo usando o certificado raiz e configura um dispositivo simulado que se conecta usando o certificado do dispositivo para atestado.
Tarefa 1: Gerar um certificado de dispositivo
Na área restrita do Azure, certifique-se de que você esteja trabalhando no diretório ~/certificates em que o script auxiliar certGen.sh foi baixado:
cd ~/certificates
Gere um certificado de dispositivo X.509 dentro da cadeia de Certificado de Autoridade de Certificação para o primeiro dispositivo usando o seguinte comando:
./certGen.sh create_device_certificate sensor-thl-001
Este comando cria um novo par .pem e .pfx de certificado X.509 do dispositivo que, que são assinados pelo certificado de AC que foi gerado anteriormente. Observe que a ID do dispositivo (sensor-thl-001) é passada para o comando create_device_certificate do script certGen.sh. Essa ID do dispositivo é definida como o nome comum ou CN=, valor do certificado do dispositivo. Esse comando gera um certificado X.509 do dispositivo folha para seu dispositivo simulado, que é usado para autenticar o dispositivo com o Serviço de Provisionamento de Dispositivos (DPS). Este módulo usa o arquivo de certificado .pfx para validar o programa que se conecta ao DPS do seu computador.
Depois que o comando create_device_certificate for concluído, o par de certificados do dispositivo X.509 gerado será nomeado new-device.cert.pfx e new-device.cert.pem, respectivamente, e estará localizado no subdiretório /certs.
Importante
Esse comando substitui qualquer certificado de dispositivo existente no subdiretório /certs. Se você quiser criar um certificado para vários dispositivos, certifique-se de salvar uma cópia de new-device.cert.pfx e new-device.cert.pem sempre que executar o comando.
Renomeie os arquivos de certificado do dispositivo para o nome do dispositivo sensor-thl-001 que você criou na última etapa usando os seguintes comandos:
mv ~/certificates/certs/new-device.cert.pfx ~/certificates/certs/sensor-thl-001-device.cert.pfx mv ~/certificates/certs/new-device.cert.pem ~/certificates/certs/sensor-thl-001-device.cert.pem
Crie e renomeie arquivos de certificado para um segundo dispositivo usando os seguintes comandos:
./certGen.sh create_device_certificate sensor-thl-002 mv ~/certificates/certs/new-device.cert.pfx ~/certificates/certs/sensor-thl-002-device.cert.pfx mv ~/certificates/certs/new-device.cert.pem ~/certificates/certs/sensor-thl-002-device.cert.pem
Baixe o primeiro certificado de dispositivo X.509 gerado do Cloud Shell para seu computador local, insira o seguinte comando:
download ~/certificates/certs/sensor-thl-001-device.cert.pfx
Observação
Fique atento a um prompt do navegador solicitando que você salve o arquivo. Selecione a mensagem Clique aqui para baixar o arquivo ou Baixar arquivo quando solicitado. O arquivo será baixado para a pasta Download do computador.
Baixe o segundo certificado de dispositivo X.509 gerado do Cloud Shell para o seu computador local usando o seguinte comando:
download ~/certificates/certs/sensor-thl-002-device.cert.pfx
Observação
Fique atento a um prompt do navegador solicitando que você salve o arquivo. Selecione a mensagem Clique aqui para baixar o arquivo ou Baixar arquivo quando solicitado. O arquivo será baixado para a pasta Download do computador.
Na próxima tarefa, você começa a criar os dispositivos simulados que usam os certificados de dispositivo X.509 para autenticar com o Serviço de Provisionamento de Dispositivos (DPS).
Tarefa 2: Configurar um dispositivo simulado
Nesta tarefa, você concluirá o seguinte:
- Criar duas pastas de projeto
- Copiar o certificado do dispositivo baixado para a pasta raiz do aplicativo
- Configurar o aplicativo no Visual Studio Code para usar o Escopo de ID do DPS
No computador de desenvolvimento, crie duas pastas no diretório de trabalho preferido:
- sensor-thl-001-device
- sensor-thl-002-device
Mova os dois arquivos de certificado que você baixou na etapa anterior para as pastas, certificando-se de que o arquivo de certificado corresponda ao nome da pasta.
Importante
Em um dispositivo de produção, o arquivo de certificado deve ser armazenado com segurança usando um módulo de segurança de hardware (HSM).
Abra a primeira pasta, sensor-thl-001-device, no Visual Studio Code.
Crie um arquivo chamado ContainerDevice.csproj.
Cole o seguinte código e salve o arquivo:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> <None Update="sensor-thl-001-device.cert.pfx" CopyToOutputDirectory="PreserveNewest" /> <PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.*" /> <PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Mqtt" Version="1.*" /> <PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Amqp" Version="1.*" /> <PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Http" Version="1.*" /> </ItemGroup> </Project>
Essa configuração garante que o arquivo de certificado sensor-thl-001-device.cert.pfx seja copiado para a pasta de build quando o código C# for compilado e disponibilizado para o programa acessar quando ele for executado.
Se você nomeou o arquivo de certificado outra coisa, atualize o valor da variável para corresponder a isso.
Crie um arquivo chamado Program.cs.
Cole o seguinte código em Program.cs:
// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Azure.Devices.Client; using Microsoft.Azure.Devices.Provisioning.Client; using Microsoft.Azure.Devices.Provisioning.Client.Transport; using Microsoft.Azure.Devices.Shared; using System; using System.IO; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using System.Security.Cryptography.X509Certificates; namespace ContainerDevice { class Program { // Azure Device Provisioning Service (DPS) ID Scope private static string dpsIdScope = "PASTE_YOUR_DPS_ID_SCOPE_HERE"; // Certificate (PFX) File Name private static string certificateFileName = "sensor-thl-001-device.cert.pfx"; // Certificate (PFX) Password private static string certificatePassword = "1234"; // NOTE: For the purposes of this example, the certificatePassword is // hard coded. In a production device, the password will need to be stored // in a more secure manner. Additionally, the certificate file (PFX) should // be stored securely on a production device using a Hardware Security Module. private const string GlobalDeviceEndpoint = "global.azure-devices-provisioning.net"; private static int telemetryDelay = 1; private static DeviceClient deviceClient; public static async Task Main(string[] args) { X509Certificate2 certificate = LoadProvisioningCertificate(); using (var security = new SecurityProviderX509Certificate(certificate)) using (var transport = new ProvisioningTransportHandlerAmqp(TransportFallbackType.TcpOnly)) { ProvisioningDeviceClient provClient = ProvisioningDeviceClient.Create(GlobalDeviceEndpoint, dpsIdScope, security, transport); using (deviceClient = await ProvisionDevice(provClient, security)) { await deviceClient.OpenAsync().ConfigureAwait(false); // INSERT Setup OnDesiredPropertyChanged Event Handling below here await deviceClient.SetDesiredPropertyUpdateCallbackAsync(OnDesiredPropertyChanged, null).ConfigureAwait(false); // INSERT Load Device Twin Properties below here var twin = await deviceClient.GetTwinAsync().ConfigureAwait(false); await OnDesiredPropertyChanged(twin.Properties.Desired, null); // Start reading and sending device telemetry Console.WriteLine("Start reading and sending device telemetry..."); await SendDeviceToCloudMessagesAsync(); await deviceClient.CloseAsync().ConfigureAwait(false); } } } private static X509Certificate2 LoadProvisioningCertificate() { var certificateCollection = new X509Certificate2Collection(); certificateCollection.Import(certificateFileName, certificatePassword, X509KeyStorageFlags.UserKeySet); X509Certificate2 certificate = null; foreach (X509Certificate2 element in certificateCollection) { Console.WriteLine($"Found certificate: {element?.Thumbprint} {element?.Subject}; PrivateKey: {element?.HasPrivateKey}"); if (certificate == null && element.HasPrivateKey) { certificate = element; } else { element.Dispose(); } } if (certificate == null) { throw new FileNotFoundException($"{certificateFileName} did not contain any certificate with a private key."); } Console.WriteLine($"Using certificate {certificate.Thumbprint} {certificate.Subject}"); return certificate; } private static async Task<DeviceClient> ProvisionDevice(ProvisioningDeviceClient provisioningDeviceClient, SecurityProviderX509Certificate security) { var result = await provisioningDeviceClient.RegisterAsync().ConfigureAwait(false); Console.WriteLine($"ProvisioningClient AssignedHub: {result.AssignedHub}; DeviceID: {result.DeviceId}"); if (result.Status != ProvisioningRegistrationStatusType.Assigned) { throw new Exception($"DeviceRegistrationResult.Status is NOT 'Assigned'"); } var auth = new DeviceAuthenticationWithX509Certificate( result.DeviceId, security.GetAuthenticationCertificate()); return DeviceClient.Create(result.AssignedHub, auth, TransportType.Amqp); } private static async Task SendDeviceToCloudMessagesAsync() { var sensor = new EnvironmentSensor(); while (true) { var currentTemperature = sensor.ReadTemperature(); var currentHumidity = sensor.ReadHumidity(); var currentPressure = sensor.ReadPressure(); var currentLocation = sensor.ReadLocation(); var messageString = CreateMessageString(currentTemperature, currentHumidity, currentPressure, currentLocation); var message = new Message(Encoding.ASCII.GetBytes(messageString)); // Add a custom application property to the message. // An IoT hub can filter on these properties without access to the message body. message.Properties.Add("temperatureAlert", (currentTemperature > 30) ? "true" : "false"); // Send the telemetry message await deviceClient.SendEventAsync(message); Console.WriteLine("{0} > Sending message: {1}", DateTime.Now, messageString); // Delay before next Telemetry reading await Task.Delay(telemetryDelay * 1000); } } private static string CreateMessageString(double temperature, double humidity, double pressure, EnvironmentSensor.Location location) { // Create an anonymous object that matches the data structure we wish to send var telemetryDataPoint = new { temperature = temperature, humidity = humidity, pressure = pressure, latitude = location.Latitude, longitude = location.Longitude }; var messageString = JsonConvert.SerializeObject(telemetryDataPoint); // Create a JSON string from the anonymous object return JsonConvert.SerializeObject(telemetryDataPoint); } private static async Task OnDesiredPropertyChanged(TwinCollection desiredProperties, object userContext) { Console.WriteLine("Desired Twin Property Changed:"); Console.WriteLine($"{desiredProperties.ToJson()}"); // Read the desired Twin Properties if (desiredProperties.Contains("telemetryDelay")) { string desiredTelemetryDelay = desiredProperties["telemetryDelay"]; if (desiredTelemetryDelay != null) { telemetryDelay = int.Parse(desiredTelemetryDelay); } // if desired telemetryDelay is null or unspecified, don't change it } // Report Twin Properties var reportedProperties = new TwinCollection(); reportedProperties["telemetryDelay"] = telemetryDelay.ToString(); await deviceClient.UpdateReportedPropertiesAsync(reportedProperties).ConfigureAwait(false); Console.WriteLine("Reported Twin Properties:"); Console.WriteLine($"{reportedProperties.ToJson()}"); } } internal class EnvironmentSensor { // Initial telemetry values double minTemperature = 20; double minHumidity = 60; double minPressure = 1013.25; double minLatitude = 39.810492; double minLongitude = -98.556061; Random rand = new Random(); internal class Location { internal double Latitude; internal double Longitude; } internal double ReadTemperature() { return minTemperature + rand.NextDouble() * 15; } internal double ReadHumidity() { return minHumidity + rand.NextDouble() * 20; } internal double ReadPressure() { return minPressure + rand.NextDouble() * 12; } internal Location ReadLocation() { return new Location { Latitude = minLatitude + rand.NextDouble() * 0.5, Longitude = minLongitude + rand.NextDouble() * 0.5 }; } } }
Localize a variável GlobalDeviceEndpoint e observe que seu valor está definido como o ponto de extremidade do dispositivo global para o Serviço de Provisionamento de Dispositivos. Você deve ver um código semelhante ao seguinte:
private const string GlobalDeviceEndpoint = "global.azure-devices-provisioning.net";
Todos os dispositivos que se conectam ao DPS são configurados com esse nome DNS de ponto de extremidade de dispositivo global.
O aplicativo ContainerDevice usa certificados X.509 como um mecanismo de atestado. Do ponto de vista do aplicativo, não importa se esse dispositivo está se conectando usando um registro de grupo em vez de um registro individual. Tudo o que o dispositivo precisa fazer é conectar-se à instância de DPS atribuída e receber as informações atribuídas do hub IoT.
Localizar a variável dpsIdScope
Atualize o valor atribuído usando o Escopo de ID do DPS que você recuperou quando criou a instância do DPS.
Quando você atualiza o código, ele deve ser semelhante ao seguinte:
private static string dpsIdScope = "0ne00000000";
Observação
Se você não tiver o valor do Escopo de ID do DPS (idScope), poderá obter uma cópia executando o comando da CLI
az iot dps show --name dps-$suffix
.Localize a variável certificateFileName e observe que seu valor está definido como o nome do arquivo de certificado do dispositivo gerado (sensor-thl-001-device.cert.pfx). Se você nomeou o arquivo de certificado outra coisa, atualize o valor da variável para corresponder a isso.
O aplicativo de dispositivo usa um certificado X.509 para autenticação. Essa variável informa ao código do dispositivo qual arquivo contém o certificado de dispositivo X.509 que ele usa ao autenticar com o Serviço de Provisionamento de Dispositivos.
Localize a variável certificatePassword e observe que seu valor está definido como a senha padrão definida pelo script certGen.sh.
A variável certificatePassword contém a senha do certificado do dispositivo X.509. Ela é definida como 1234, que é a senha padrão usada pelo script auxiliar certGen.sh ao gerar os certificados X.509.
Importante
Para fins deste laboratório, a senha é codificada em código. Em um cenário de produção, a senha precisa ser armazenada de maneira mais segura, como em um Azure Key Vault. Além disso, o arquivo de certificado (PFX) deve ser armazenado com segurança em um dispositivo de produção usando um módulo de segurança de hardware (HSM).
O HSM é usado para armazenamento seguro baseado em hardware de segredos do dispositivo e é a forma mais segura de armazenamento secreto. Os certificados X.509 e os tokens SAS podem ser armazenados no HSM. Os HSMs podem ser usados com todos os mecanismos de atestado compatíveis com o serviço de provisionamento.
Abra o menu Arquivo do Visual Studio Code e selecione Salvar.
Copie sensor-thl-001-device.cert.pfx da pasta Downloads para a pasta sensor-thl-001-device.
Copie sensor-thl-002-device.cert.pfx da pasta Downloads para a pasta sensor-thl-002-device.
Verificar seu trabalho
No Visual Studio, abra o menu Terminal e selecione Novo terminal.
No prompt de comando do Terminal, certifique-se de que o diretório de trabalho atual seja a pasta \sensor-thl-001-device.
No prompt de comando do Terminal do Visual Studio, crie o código para verificar se há erros.
dotnet build ContainerDevice.csproj
Se você vir erros de build listados, corrija-os antes de continuar para o próximo exercício.