Exercice : configurer un appareil simulé avec un certificat X.509

Effectué

Dans cet exercice, vous générez un certificat d’appareil en utilisant le certificat racine et configurez un appareil simulé qui se connecte en tirant parti du certificat d’appareil pour l’attestation.

Tâche 1 : Générer un certificat d’appareil

  1. Dans le bac à sable Azure, vérifiez que vous travaillez dans le répertoire ~/certificates où le script d’assistance certGen.sh a été téléchargé :

    cd ~/certificates
    
  2. Générez un certificat d’appareil X.509 dans la chaîne de certificats d’autorité de certification pour le premier appareil en utilisant la commande suivante :

    ./certGen.sh create_device_certificate sensor-thl-001
    

    Cette commande crée une paire de certificats .pem et .pfx d’appareil X.509 signés par le certificat d’autorité de certification précédemment généré. Notez que l’ID d’appareil (sensor-thl-001) est passé à la commande create_device_certificate du script certGen.sh. Cet ID d’appareil est défini comme nom commun, ou CN=, valeur du certificat d’appareil. Cette commande génère un certificat feuille X.509 d’appareil pour votre appareil simulé, qui est utilisé pour authentifier l’appareil auprès du Service Azure IoT Hub Device Provisioning (DPS). Ce module utilise le fichier de certificat .pfx pour valider le programme qui se connecte au service DPS à partir de votre ordinateur.

    Une fois la commande create_device_certificate terminée, la paire de certificats d’appareil X.509 générée est nommée new-device.cert.pfx et new-device.cert.pem respectivement, et se trouve dans le sous-répertoire /certs.

    Important

    Cette commande remplace tout certificat d’appareil existant dans le sous-répertoire /certs. Si vous souhaitez créer un certificat pour plusieurs appareils, veillez à enregistrer une copie de new-device.cert.pfx et new-device.cert.pem lors de chaque exécution de la commande.

  3. Renommez les fichiers de certificat d’appareil en nom d’appareil sensor-thl-001 créé à la dernière étape en utilisant les commandes suivantes :

    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
    
  4. Créez et renommez des fichiers de certificat pour un deuxième appareil en tirant parti des commandes suivantes :

    ./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
    
  5. Téléchargez le premier certificat d’appareil X.509 généré à partir de Cloud Shell sur votre ordinateur local, entrez la commande suivante :

    download ~/certificates/certs/sensor-thl-001-device.cert.pfx
    

    Remarque

    Ne manquez pas une invite de navigateur vous demandant d’enregistrer le fichier. Sélectionnez le message Cliquer ici pour télécharger votre fichier. ou Télécharger le message lorsque vous y êtes invité. Le fichier sera téléchargé dans le fichier Téléchargement de votre ordinateur.

  6. Téléchargez le deuxième certificat d’appareil X.509 généré à partir de Cloud Shell sur votre ordinateur local en utilisant la commande suivante :

    download ~/certificates/certs/sensor-thl-002-device.cert.pfx
    

    Remarque

    Ne manquez pas une invite de navigateur vous demandant d’enregistrer le fichier. Sélectionnez le message Cliquer ici pour télécharger votre fichier. ou Télécharger le message lorsque vous y êtes invité. Le fichier sera téléchargé dans le fichier Téléchargement de votre ordinateur.

Dans la tâche suivante, vous commencez à générer les appareils simulés qui utilisent les certificats d’appareil X.509 pour s’authentifier auprès du Service Azure IoT Hub Device Provisioning (DPS).

Tâche 2 : Configurer un appareil simulé

Dans cette tâche, vous effectuez les opérations suivantes :

  • Créer deux dossiers de projet
  • Copier le certificat d’appareil téléchargé dans le dossier racine de l’application
  • Configurer l’application dans Visual Studio Code pour utiliser l’étendue de l’ID DPS
  1. Sur votre ordinateur de développement, créez deux dossiers dans votre répertoire de travail préféré :

    • sensor-thl-001-device
    • sensor-thl-002-device
  2. Déplacez les deux fichiers de certificat que vous avez téléchargés à l’étape précédente dans les dossiers, en vérifiant que le fichier de certificat correspond au nom du dossier.

    Important

    Sur un appareil de production, le fichier de certificat doit être stocké en toute sécurité en utilisant un module de sécurité matériel (HSM).

  3. Ouvrez le premier dossier, sensor-thl-001-device, dans Visual Studio Code.

  4. Créez un fichier nommé ContainerDevice.csproj.

  5. Collez le code suivant et enregistrez le fichier :

    <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>   
    

    Cette configuration veille à ce que le fichier de certificat sensor-thl-001-device.cert.pfx soit copié dans le dossier de build lorsque le code C# est compilé et mis à la disposition du programme pour y accéder lors de son exécution.

    Si vous avez donné un autre nom au fichier de certificat, mettez à jour la valeur de la variable pour qu’elle corresponde.

  6. Créez un fichier nommé Program.cs.

  7. Collez le code suivant dans 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 };
            }
        }
    }
    
  8. Recherchez la variable GlobalDeviceEndpoint et notez que sa valeur est définie sur le point de terminaison global de l’appareil pour le Service Azure IoT Hub Device Provisioning. Vous devriez voir un code similaire à ce qui suit :

    private const string GlobalDeviceEndpoint = "global.azure-devices-provisioning.net";
    

    Tous les appareils se connectant au DPS sont configurés avec ce nom DNS de point de terminaison d’appareil global.

    L’application ContainerDevice utilise des certificats X.509 en tant que mécanisme d’attestation. Du point de vue de l’application, il n’est pas important que cet appareil se connecte en tirant parti d’une inscription de groupe plutôt que d’une inscription individuelle. Tout ce que l’appareil doit effectuer consiste à se connecter à son instance DPS affectée et à recevoir ses informations de hub IoT affectées.

  9. Rechercher la variable dpsIdScope

  10. Mettez à jour la valeur affectée à l’aide de l’Étendue de l’ID DPS que vous avez récupérée lors de la création de l’instance DPS.

    Quand vous mettez à jour votre code, il doit être semblable à ce qui suit :

    private static string dpsIdScope = "0ne00000000";
    

    Remarque

    Si vous n’avez pas la valeur d’étendue de l’ID DPS (idScope), vous pouvez obtenir une copie en exécutant la commande CLI az iot dps show --name dps-$suffix.

  11. Recherchez la variable certificateFileName et notez que sa valeur est définie sur le nom du fichier de certificat d’appareil que vous avez généré (sensor-thl-001-device.cert.pfx). Si vous avez donné un autre nom au fichier de certificat, mettez à jour la valeur de la variable pour qu’elle corresponde.

    L’application d’appareil utilise un certificat X.509 pour l’authentification. Cette variable indique au code de l’appareil le fichier qui contient le certificat d’appareil X.509 utilisé lors de l’authentification auprès du Service Azure IoT Hub Device Provisioning.

  12. Recherchez la variable certificatePassword et notez que sa valeur est définie sur le mot de passe par défaut défini par le script certGen.sh.

    La variable certificatePassword contient le mot de passe du certificat d’appareil X.509. Il est défini sur 1234, qui est le mot de passe par défaut utilisé par le script d’assistance certGen.sh lors de la génération des certificats X.509.

    Important

    Pour les besoins de ce labo, le mot de passe est codé en dur. Dans un scénario de production, le mot de passe doit être stocké de manière plus sécurisée, par exemple dans un coffre de clés Azure. En outre, le fichier de certificat (PFX) doit être stocké en toute sécurité sur un appareil de production en utilisant un module de sécurité matériel (HSM).

    HSM est utilisé pour le stockage matériel sécurisé des secrets de l’appareil. C’est la forme de stockage de secrets la plus sécurisée. Les certificats X.509 et les jetons SAP peuvent être stockés dans le module HSM. Les modules HSM peuvent être utilisés avec tous les mécanismes d’attestation que le service d’approvisionnement prend en charge.

  13. Ouvrez le menu Fichier de Visual Studio Code, puis sélectionnez Enregistrer.

  14. Copiez sensor-thl-001-device.cert.pfx à partir de votre dossier Téléchargements dans le dossier sensor-thl-001-device.

  15. Copiez sensor-thl-002-device.cert.pfx à partir de votre dossier Téléchargements dans le dossier sensor-thl-002-device.

Vérifier votre travail

  1. Dans Visual Studio, ouvrez le menu Terminal, puis sélectionnez Nouveau terminal.

  2. À l’invite de commandes Terminal, vérifiez que le répertoire de travail actuel est le dossier \sensor-thl-001-device.

  3. À l’invite de commandes du Terminal Visual Studio, générez le code pour rechercher les erreurs.

    dotnet build ContainerDevice.csproj
    
  4. Si des erreurs de build sont répertoriées, corrigez-les avant de passer à l’exercice suivant.