Для работы с интернетом вещей зачастую используются различные более серьезные девайсы, способные поддерживать NodeJS. Примером такого девайса может быть одна из самых популярных мэйкерских плат Raspberry PI. Поэтому я не мог обойти стороной эту тему и решил разобраться с тем как поддерживающие NodeJS девайсы могут работать с Azure IoT hub.

Кроме того, под катом рассматривается возможность защиты соединения SSL сертификатом и процесс создания самоподписанного сертификата.

Скачиваем и устанавливаем NodeJS. Англоязычный мануал как это сделать доступен здесь:
Installing Node.js via package manager

Создаем новую директорию. Открываем CMD или Bash, и переходим в созданную директорию
Выполняем команду

npm init

В результате ответов на вопросы будет создан package.json файл.

Устанавливаем с помощью команды пару пакетов:

npm install azure-iot-device azure-iot-device-mqtt –save

В данном примере мы используем протокол MQTT, который не содержит в себе лишней информации, имеет не так много возможностей, но зато специально предназначен для IoT устройств.

Создаем какой-нибудь JS файл. Допустим, файл с именем index.js. Теперь, давайте добавим в файл следующий код и рассмотрим его:

'use strict';
// с помощью require('package-name') можно сослаться на ранее установленный пакет
var clientFromConnectionString = require('azure-iot-device-mqtt').clientFromConnectionString;
var Message = require('azure-iot-device').Message;

var connectionString = 'HostName=ArduinoDemoHub.azure-devices.net;DeviceId=ArduinoAzureTwin;SharedAccessKey=Fqve19DXyMOx3vQI63eHxW0bK6VmT+R6iqPK23vhdiQ=';
var client = clientFromConnectionString(connectionString);

function printResultFor(op) {
  return function printResult(err, res) {
    if (err) console.log(op + ' error: ' + err.toString());
    if (res) console.log(op + ' status: ' + res.constructor.name);
  };
}

var connectCallback = function (err) {
  if (err) {
    console.log('Could not connect: ' + err);
  } else {
    console.log('Client connected');
   // «Подписываемся» вызов метода writeLine с помощью сообщения из облака
    client.onDeviceMethod('writeLine', onWriteLine);

    // Создаем сообщение и отправляем его в IoT Hub каждую секунду
    setInterval(function(){
        var iotdata = Math.round((Math.random() * 25));          
        var data = JSON.stringify({ deviceId: 'ArduinoAzureTwin', iotdata: iotdata });
        var message = new Message(data);
        console.log("Sending message: " + message.getData());
        client.sendEvent(message, printResultFor('send'));
    }, 1000);
  }
};

function onWriteLine(request, response) {
    response.send(200, 'Input was written to log.', function(err) {
        if(err) {
            console.error('An error ocurred when sending a method response:\n' 
                                                             + err.toString());
        } else {
            console.log('Response to method \'' 
                             + request.methodName + '\' sent successfully.' );
        }
    });
}

client.open(connectCallback);

Разумеется, что у вас должен был быть создан IoT hub и строку подключения из кода вы должны заменить на свою. В моем примере отправляются данные с именами deviceId и iotdata. Вы можете отправлять какие-то свои данные.

Запустить код на выполнение можно с помощью команды

node index.js

Если все сделано правильно, то результат должен быть должен быть примерно таким:



Ссылки на официальные мануалы:

Подключение виртуального устройства к Центру Интернета вещей с помощью Node
Использование прямых методов на устройстве Интернета вещей (Node.js)

Все это работает, но сделано не особо хорошо с точки зрения безопасности.
Для того чтобы защитить подключение можно использовать TLS сертификаты X.509. В продакшн, конечно же, лучше использовать сертификаты, выданные каким-либо удостоверяющим центром. Но для тестирования можно использовать и самоподписанные сертификаты. Давайте создадим такой и защитим соединение.

Для создания сертификата я изначально думал использовать приложение makecert, но оказалось, что оно уже считается устаревшим. Так что решил использовать скрипты PowerShell.

Открывааем Powershell с правами админа. Выполняем следующую команду, указав после CN= имя, которым вы хотите назвать сертификат:

$cert = New-SelfSignedCertificate -Type Custom -KeySpec Signature `
-Subject "CN=AlexRootCert" -KeyExportPolicy Exportable `
-HashAlgorithm sha256 -KeyLength 2048 `
-CertStoreLocation "Cert:\CurrentUser\My" -KeyUsageProperty Sign -KeyUsage CertSign

Теперь можно открыть утилиту certmgr.msc и найти свой сертификат в папке Certificates — Current User/Personal/Certificates.

Сертификат необходимо экспортировать следующим образом (без приватного ключа):







Этот сертификат мы после загрузим в IoT Hub.

После генерации корневого сертификата желательно не закрывать окно Powershell. Если вы не закрыли его, то сгенерировать клиентский сертификат будет довольно просто для вас. Ведь вы сможете использовать переменную $cert.

Если закрыли, то читайте официальную документацию (ищите там Example 2). Или открывайте спойлер и читайте мое описание того как можно объявить переменную cert.

как можно объявить переменную cert
Выполняем команду

Get-ChildItem -Path "Cert:\CurrentUser\My"

Получаем примерно вот такой ответ:

PSParentPath: Microsoft.PowerShell.Security\Certificate::CurrentUser\My
Thumbprint Subject
— — FDF386B2AE761CBDD5A111CEF047CC5419F9425A CN=AlexRootCert

Их этого ответа копируем Thumbprint сертификата и вставляем в команду:
$cert = Get-ChildItem -Path "Cert:\CurrentUser\My\FDF386B2AE761CBDD5A111CEF047CC5419F9425A"


Создаем клиентский сертификат. Ничего кроме имени создаваемого сертификата в команде менять не нужно.

New-SelfSignedCertificate -Type Custom -KeySpec Signature `
-Subject "CN=AlexClientCert" -KeyExportPolicy Exportable `
-HashAlgorithm sha256 -KeyLength 2048 `
-CertStoreLocation "Cert:\CurrentUser\My" `
-Signer $cert -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.2")

Теперь давайте загрузим первый созданный сертификат в IoT hub. Для загрузки корневого сертификата заходим в IoT hub в раздел сертификатов.





Так как он у нас самоподписан, то он будет отображен как «Unverified». Подтверждение сертификата называется Proof of possession. Открываем окно свойств сертификата и прокручиваем вниз к последним двум полям.



Нажимаем кнопочку «Generate Verification Code» и копируем созданный код. Опять открываем PowerShell и запускаем на выполнение следующий код:

function New-CASelfsignedCertificate([string]$subjectName, [object]$signingCert, [bool]$isASigner=$true)
{
 # Build up argument list
 $selfSignedArgs =@{"-DnsName"=$subjectName; 
                    "-CertStoreLocation"="cert:\LocalMachine\My";
                    "-NotAfter"=(get-date).AddDays(30); 
                   }

 if ($isASigner -eq $true)
 {
     $selfSignedArgs += @{"-KeyUsage"="CertSign"; }
     $selfSignedArgs += @{"-TextExtension"= @(("2.5.29.19={text}ca=TRUE&pathlength=12")); }
 }
 else
 {
     $selfSignedArgs += @{"-TextExtension"= @("2.5.29.37={text}1.3.6.1.5.5.7.3.2,1.3.6.1.5.5.7.3.1", "2.5.29.19={text}ca=FALSE&pathlength=0")  }
 }

 if ($signingCert -ne $null)
 {
     $selfSignedArgs += @{"-Signer"=$signingCert }
 }

 if ($useEcc -eq $true)
 {
     $selfSignedArgs += @{"-KeyAlgorithm"="ECDSA_nistP256";
                      "-CurveExport"="CurveName" }
 }

 # Now use splatting to process this
 Write-Host ("Generating certificate {0} which is for prototyping, NOT PRODUCTION.  It will expire in 30 days." -f $subjectName)
 write (New-SelfSignedCertificate @selfSignedArgs)
}

function New-CAVerificationCert([string]$requestedSubjectName)
{
    $cnRequestedSubjectName = ("CN={0}" -f $requestedSubjectName)
    $verifyRequestedFileName = "D:\verifyCert4.cer"

    $сert = Get-ChildItem -Path "Cert:\CurrentUser\My\FDF386B2AE761CBDD5A111CEF047CC5419F9425A"

    Write-Host "Using Signing Cert:::" 
    Write-Host $сert

    $verifyCert = New-CASelfsignedCertificate $cnRequestedSubjectName $сert $false

    Export-Certificate -cert $verifyCert -filePath $verifyRequestedFileName -Type Cert
    if (-not (Test-Path $verifyRequestedFileName))
    {
        throw ("Error: CERT file {0} doesn't exist" -f $verifyRequestedFileName)
    }

    Write-Host ("Certificate with subject {0} has been output to {1}" -f $cnRequestedSubjectName, (Join-Path (get-location).path $verifyRequestedFileName)) 
}
New-CAVerificationCert "8957B1DCD5D19DDFD06BFCC90A9B740C9EF7B4DBEC84C5AB"

В этом коде в последнюю строку вставляем полученный из Azure код верификации. И в строке:

$сert = Get-ChildItem -Path "Cert:\CurrentUser\My\FDF386B2AE761CBDD5A111CEF047CC5419F9425A"

Вставляем thumbprint, который мы получили ранее из Root сертификата. Или же, если не закрывали Powershell с самого начала, то эту строку можно удалить.

Если есть желание, то можно изменить директорию и имя создаваемого сертификата. После того как файл будет создан, загрузим его в IoT Hub.

Теперь необходимо создать двойник устройства. В IoT Hub-е открываем пункт меню «IoT Devices». Выбираем тип аутентификации X.509 Self-Signed и понимаем, что от нас просят какой-то отпечаток сертификата. Причем даже 2 отпечатка.



Заходим в менеджер сертификатов certmgr.msc и кликаем на клиентский сертификат дважды. Откроется окно свойств в котором можно найти то что нужно:



Значение Thumbprint копируем и вставляем в окно создания двойника устройства. Второй отпечаток запрашивается для особых сценариев, в которых сертификат может быть заменен на лету на запасной.

Это еще не все. Нам необходимо экспортировать клиентский сертификат без приватного ключа (в виде cer) и с приватным ключом (в формате pxf). Из экспортированного клиентского сертификата в формате pxf необходимо создать файл ключа в формате pem.

Клиентский сертификат без приватного ключа экспортируется точно так же, как мы уже экспортировали корневой сертификат. А вот в формат pfx сертификат экспортируется точно так же, как экспортируется корневой сертификат для того чтобы установить его на другом компьютере:





Если не включить пункт Enable certificate privacy, то содержимое сертификата останется не зашифрованным. Защита будет применена только к приватному ключу. Содержимое сертификата можно будет просмотреть с помощью certutil.

Export all extended properties относится только к платформе Microsoft и экспортирует некоторую дополнительную информацию.



Чтобы создать pem файл нам необходим OpenSSL. Для установки OpenSSL на Windows можно воспользоваться WSL. Установить Ubuntu из Microsoft Store и далее уже установить OpenSSL

sudo apt-get update
sudo apt-get install openssl

Теперь необходимо с помощью команды cd зайти в директорию с сертификатом и выполнить такую команду:

openssl pkcs12 -in alex-client-cert.pfx -out key.pem -nodes

В команде указано имя существующего сертификата и имя файла pem, который будет создан.

Альтернативно можно использовать следующую утилиту: Win64 OpenSSL v1.0.2n
Зайти в директорию установленной утилиты (у меня это C:\OpenSSL-Win64\bin) и выполнить следующую команду:

.\openssl pkcs12 -in D:\iotapp\alex-client-cert.pfx -out D:\iotapp\key.pem -nodes

Остается изменить наш код на код защищенный сертификатом. В качестве примера я использовал следующий: simple_sample_device_x509.js

'use strict';
var clientFromConnectionString = require('azure-iot-device-mqtt').clientFromConnectionString;
var Message = require('azure-iot-device').Message;
var connectionString = 'HostName=ArduinoDemoHub.azure-devices.net;DeviceId=NodeTwin;x509=true';

var fs = require('fs');
var certFile = 'alex-client-cert.cer';
var keyFile = 'key.pem';
var passphrase = 'KJu86Hfjw3jG';

var client = clientFromConnectionString(connectionString);

function printResultFor(op) {
  return function printResult(err, res) {
    if (err) console.log(op + ' error: ' + err.toString());
    if (res) console.log(op + ' status: ' + res.constructor.name);
  };
}

var connectCallback = function (err) {
  if (err) {
    console.log('Could not connect: ' + err);
  } else {
    console.log('Client connected');

    client.onDeviceMethod('writeLine', onWriteLine);

    // Create a message and send it to the IoT Hub every second
    setInterval(function(){
        var iotdata = Math.round((Math.random() * 25));          
        var data = JSON.stringify({ deviceId: 'ArduinoAzureTwin', iotdata: iotdata });
        var message = new Message(data);
        console.log("Sending message: " + message.getData());
        client.sendEvent(message, printResultFor('send'));
    }, 1000);
  }
};

function onWriteLine(request, response) {
    console.log('sdfdsf');

    response.send(200, 'Input was written to log.', function(err) {
        if(err) {
            console.error('An error ocurred when sending a method response:\n' 
                                           + err.toString());
        } else {
            console.log('Response to method \'' 
                             + request.methodName + '\' sent successfully.' );
        }
    });
}

 var options = {
   cert : fs.readFileSync(certFile, 'utf-8').toString(),
   key : fs.readFileSync(keyFile, 'utf-8').toString(),
   passphrase: passphrase
 };

client.setOptions(options);
client.open(connectCallback);

Ссылка на официальный мануал:

Настройка сертификата безопасности X.509 в Центре Интернета вещей Azure

Комментарии (2)


  1. dmitry_dvm
    17.01.2018 16:14

    >$selfSignedArgs += @{"-TextExtension"= @(«2.5.29.37={text}1.3.6.1.5.5.7.3.2,1.3.6.1.5.5.7.3.1», «2.5.29.19={text}ca=FALSE&pathlength=0»)
    Что это за магические числа?


    1. asommer Автор
      17.01.2018 16:30

      Это OID (Object identifier)