연습 - X.509 인증서로 시뮬레이션된 디바이스 구성
이 연습에서는 루트 인증서를 사용하여 디바이스 인증서를 생성하고, 증명용 디바이스 인증서를 사용하여 연결하는 시뮬레이션된 디바이스를 구성합니다.
작업 1: 디바이스 인증서 생성
Azure 샌드박스에서 certGen.sh 도우미 스크립트가 다운로드된 ~/certificates 디렉터리에서 작업하고 있는지 확인합니다.
cd ~/certificates
다음 명령을 사용하여 첫 번째 디바이스에 대한 CA 인증서 체인 내에서 X.509 디바이스 인증서를 생성합니다.
./certGen.sh create_device_certificate sensor-thl-001
이 명령은 이전에 만들어진 CA 인증서로 서명된 새 디바이스 X.509 인증서 .pem 및 .pfx 쌍을 만듭니다. 디바이스 ID(sensor-thl-001)가 certGen.sh 스크립트의 create_device_certificate 명령으로 전달됩니다. 이 디바이스 ID는 디바이스 인증서의 일반 이름 또는 CN= 값으로 설정됩니다. 이 명령은 DPS(Device Provisioning Service)로 디바이스를 인증하는 데 사용되는 시뮬레이션된 디바이스에 대한 리프 디바이스 X.509 인증서를 생성합니다. 이 모듈은 .pfx 인증서 파일을 사용하여 컴퓨터에서 DPS에 연결하는 프로그램의 유효성을 검사합니다.
create_device_certificate 명령이 완료되면 만들어진 X.509 디바이스 인증서 쌍의 이름은 new-device.cert.pfx 및 new-device.cert.pem이 됩니다. 각각 /certs 하위 디렉터리에 있습니다.
Important
이 명령은 /certs 하위 디렉터리의 기존 디바이스 인증서를 덮어씁니다. 여러 디바이스에 대한 인증서를 만들려면 명령을 실행할 때마다 new-device.cert.pfx 및 new-device.cert.pem의 복사본을 저장해야 합니다.
다음 명령을 사용하여 디바이스 인증서 파일의 이름을 마지막 단계에서 만든 sensor-thl-001 디바이스 이름으로 바꿉니다.
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
다음 명령을 사용하여 두 번째 디바이스에 대한 인증서 파일을 만들고 이름을 바꿉니다.
./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
처음 생성된 X.509 디바이스 인증서를 Cloud Shell에서 로컬 컴퓨터로 다운로드하고 다음 명령을 입력합니다.
download ~/certificates/certs/sensor-thl-001-device.cert.pfx
참고 항목
파일을 저장하라는 브라우저 프롬프트를 확인합니다. 메시지가 표시되면 파일을 다운로드하려면 여기를 클릭하세요. 또는 파일 다운로드 메시지를 선택합니다. 파일이 컴퓨터의 다운로드 폴더에 다운로드됩니다.
다음 명령을 사용하여 두 번째로 생성된 X.509 디바이스 인증서를 Cloud Shell에서 로컬 컴퓨터로 다운로드합니다.
download ~/certificates/certs/sensor-thl-002-device.cert.pfx
참고 항목
파일을 저장하라는 브라우저 프롬프트를 확인합니다. 메시지가 표시되면 파일을 다운로드하려면 여기를 클릭하세요. 또는 파일 다운로드 메시지를 선택합니다. 파일이 컴퓨터의 다운로드 폴더에 다운로드됩니다.
다음 작업에서는 X.509 디바이스 인증서를 사용하여 DPS(Device Provisioning Service)에 인증하는 시뮬레이션된 디바이스 빌드를 시작합니다.
작업 2: 시뮬레이션된 디바이스 구성
이 작업에서는 다음을 완료합니다.
- 두 개의 프로젝트 폴더 만들기
- 다운로드한 디바이스 인증서를 애플리케이션 루트 폴더에 복사
- DPS ID 범위를 사용하도록 Visual Studio Code에서 애플리케이션을 구성합니다.
개발 컴퓨터에서 원하는 작업 디렉터리에 두 개의 폴더를 만듭니다.
- sensor-thl-001-device
- sensor-thl-002-device
이전 단계에서 다운로드한 두 개의 인증서 파일을 폴더로 이동하고 인증서 파일이 폴더 이름과 일치하는지 확인합니다.
Important
프로덕션 디바이스에서는 HSM(하드웨어 보안 모듈)을 사용하여 인증서 파일을 안전하게 저장해야 합니다.
Visual Studio Code에서 첫 번째 폴더인 sensor-thl-001-device를 엽니다.
ContainerDevice.csproj라는 파일을 만듭니다.
다음 코드를 붙여넣고 파일을 저장합니다.
<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>
이 구성을 사용하면 C# 코드가 컴파일될 때 sensor-thl-001-device.cert.pfx 인증서 파일이 빌드 폴더에 복사됩니다. 따라서 프로그램이 실행될 때 해당 파일에 액세스할 수 있습니다.
인증서 파일 이름을 다른 이름으로 지정한 경우 일치하도록 변수 값을 업데이트합니다.
Program.cs라는 파일을 만듭니다.
다음 코드를 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 }; } } }
GlobalDeviceEndpoint 변수를 찾아 해당 값이 Device Provisioning Service의 전역 디바이스 엔드포인트로 설정되어 있는지 확인합니다. 다음과 유사한 코드가 표시됩니다.
private const string GlobalDeviceEndpoint = "global.azure-devices-provisioning.net";
DPS에 연결하는 모든 디바이스는 이 전역 디바이스 엔드포인트 DNS 이름으로 구성됩니다.
ContainerDevice 애플리케이션은 X.509 인증서를 증명 메커니즘으로 사용합니다. 애플리케이션의 관점에서 개별 등록이 아닌 그룹 등록을 사용하여 이 디바이스가 연결된다는 것은 중요하지 않습니다. 디바이스는 할당된 DPS 인스턴스에 연결하고 할당된 IoT Hub 정보를 받기만 하면 됩니다.
dpsIdScope 변수를 찾습니다.
DPS 인스턴스를 만들 때 검색한 DPS ID 범위를 사용하여 할당된 값을 업데이트합니다.
코드를 업데이트하면 다음과 유사하게 표시됩니다.
private static string dpsIdScope = "0ne00000000";
참고 항목
DPS ID 범위(idScope) 값이 없으면 CLI 명령
az iot dps show --name dps-$suffix
를 실행하여 복사본을 가져올 수 있습니다.certificateFileName 변수를 찾아 해당 값이 생성한 디바이스 인증서 파일(sensor-thl-001-device.cert.pfx)의 이름으로 설정되어 있는지 확인합니다. 인증서 파일 이름을 다른 이름으로 지정한 경우 일치하도록 변수 값을 업데이트합니다.
디바이스 애플리케이션은 인증을 위해 X.509 인증서를 사용합니다. 이 변수는 Device Provisioning Service로 인증할 때 사용하는 X.509 디바이스 인증서가 포함된 파일을 디바이스 코드에 알려 줍니다.
certificatePassword 변수를 찾아 해당 값이 certGen.sh 스크립트에 의해 정의된 기본 암호로 설정되어 있는지 확인합니다.
certificatePassword 변수에는 X.509 디바이스 인증서용 암호가 포함되어 있습니다. X.509 인증서를 생성할 때 certGen.sh 도우미 스크립트에서 사용하는 기본 암호인 1234로 설정됩니다.
Important
이 랩을 위해 암호는 하드 코딩됩니다. 프로덕션 시나리오에서는 암호를 Azure Key Vault와 같이 보다 안전한 방식으로 저장해야 합니다. 또한 인증서 파일(PFX)은 HSM(하드웨어 보안 모듈)을 사용하여 프로덕션 디바이스에 안전하게 저장되어야 합니다.
HSM은 디바이스 비밀을 안전하게 하드웨어 기반으로 저장하는 데 사용되며 가장 안전한 형태의 비밀 스토리지입니다. X.509 인증서 및 SAS 토큰은 HSM에 저장될 수 있습니다. HSM은 프로비저닝 서비스가 지원하는 모든 증명 메커니즘과 함께 사용할 수 있습니다.
Visual Studio Code 파일 메뉴를 열고 저장을 선택합니다.
다운로드 폴더에서 sensor-thl-001-device.cert.pfx를 sensor-thl-001-device 폴더로 복사합니다.
다운로드 폴더에서 sensor-thl-002-device.cert.pfx를 sensor-thl-002-device 폴더로 복사합니다.
작업 확인
Visual Studio에서 터미널 메뉴를 연 다음 새 터미널을 선택합니다.
터미널 명령 프롬프트에서 현재 작업 디렉터리가 \sensor-thl-001-device 폴더인지 확인합니다.
Visual Studio 터미널 명령 프롬프트에서 오류를 확인하는 코드를 빌드합니다.
dotnet build ContainerDevice.csproj
나열된 빌드 오류가 표시되면 다음 연습을 계속하기 전에 수정합니다.