はじめに
最近、Renesas RXファミリ用 FreeRTOS-LTSサンプルプログラム (iot-reference-rx)がGitHub上に公開されました (iot-reference-rxには、FreeRTOS-v202210.01-LTSのライブラリを使用した各種デモプログラム/プロジェクトファイルが配置されています)。
このiot-reference-rxで試すことができるデモプログラムは以下の3つです
これらのデモプログラムは複数を同時に実行することが可能なようです。
- PubSub (AWS IoT Coreに簡単なMQTTメッセージを送信する)
- OTA (AWS IoT Coreを使用してファームウェアアップデートを実行する)
- Fleet Provisioning (共通の証明書/秘密鍵を使用してデバイス個別の証明書を取得する)
今回はiot-reference-rxを変更してデバイス個別の証明書を更新するデモを追加しました。
これを使えば有効期限が近い証明書を更新することができます。
既にAWS公式に証明書更新のためのアプローチが公開されています。
今回はこのアプローチに沿ってデモの実装を進めました。
実装したデモは以下GitHubリポジトリに公開しています。
- nizuki926/iot-reference-rx
- Branch Name : v202210.01-lts-rx-1.1.2-update-cert
上記アプローチはスケジュールを組んで証明書の有効期限を確認しています。
今回は簡易的なデモで単純にデモ実行時に、デバイスからAWS IoT CoreにCSRリクエストを送信して、AWS Lambda上で証明書を更新して、作成された証明書をデバイスに送り返す実装になっています。
準備した環境は以下の通りです。
- CK-RX65N (評価ボード)
- e2 studio 2023-10 (統合開発環境)
- CC-RX V3.05.00 (RXマイコン用コンパイラ)
証明書更新デモの実装
corePKCS11に更新用の証明書/鍵ペアの項目を増やす
更新された証明書および鍵ペアをデータフラッシュに格納するためcorePKCS11ライブラリに更新された証明書および鍵ペアの項目を増やします。
以下のソースコード変更に合わせてcore_pkcs11_config.h
の各種設定も見直します。
enum eObjectHandles
{
/* ... */
#if pkcs11configMAX_NUM_OBJECTS >= 10
eAwsUpdateDevicePrivateKey,
#endif
#if pkcs11configMAX_NUM_OBJECTS >= 11
eAwsUpdateDevicePublicKey,
#endif
#if pkcs11configMAX_NUM_OBJECTS >= 12
eAwsUpdateDeviceCertificate,
#endif
};
uint8_t g_object_handle_dictionary[pkcs11configMAX_NUM_OBJECTS][pkcs11configMAX_LABEL_LENGTH + 1] =
{
/* ... */
#if pkcs11configMAX_NUM_OBJECTS >= 10
pkcs11configLABEL_UPDATE_DEVICE_PRIVATE_KEY_FOR_TLS,
#endif
#if pkcs11configMAX_NUM_OBJECTS >= 11
pkcs11configLABEL_UPDATE_DEVICE_PUBLIC_KEY_FOR_TLS,
#endif
#if pkcs11configMAX_NUM_OBJECTS >= 12
pkcs11configLABEL_UPDATE_DEVICE_CERTIFICATE_FOR_TLS,
#endif
};
またKVStoreの項目にも更新された証明書および鍵ペアの項目を追加します。
ただし、今回更新された証明書および鍵ペアはCLIで入力する必要がないため、この変更は必要ないかもしれません。
#define KVSTORE_KEYS \
{ \
[ KVS_CORE_THING_NAME ] = "thing_name", \
[ KVS_CORE_MQTT_ENDPOINT ] = "mqtt_endpoint", \
[ KVS_DEVICE_CERT_ID ] = pkcs11configLABEL_DEVICE_CERTIFICATE_FOR_TLS, \
[ KVS_DEVICE_PRIVKEY_ID ] = pkcs11configLABEL_DEVICE_PRIVATE_KEY_FOR_TLS, \
[ KVS_DEVICE_PUBKEY_ID ] = pkcs11configLABEL_DEVICE_PUBLIC_KEY_FOR_TLS, \
[ KVS_ROOT_CA_ID ] = "root_ca_id", \
[ KVS_TEMPLATE_NAME ] = "template_name", \
[ KVS_CLAIM_CERT_ID ] = pkcs11configLABEL_CLAIM_CERTIFICATE, \
[ KVS_CLAIM_PRIVKEY_ID ] = pkcs11configLABEL_CLAIM_PRIVATE_KEY, \
[ KVS_CODE_SIGN_CERT_ID ] = "code_sign_cert_id", \
[ KVS_UPDATE_DEVICE_CERT_ID ] = pkcs11configLABEL_UPDATE_DEVICE_CERTIFICATE_FOR_TLS, \
[ KVS_UPDATE_DEVICE_PRIVKEY_ID ] = pkcs11configLABEL_UPDATE_DEVICE_PRIVATE_KEY_FOR_TLS, \
[ KVS_UPDATE_DEVICE_PUBKEY_ID ] = pkcs11configLABEL_UPDATE_DEVICE_PUBLIC_KEY_FOR_TLS, \
}
#define CLICMDKEYS \
{ \
[ KVS_CORE_THING_NAME ] = "thingname", \
[ KVS_CORE_MQTT_ENDPOINT ] = "endpoint", \
[ KVS_DEVICE_CERT_ID ] = "cert", \
[ KVS_DEVICE_PRIVKEY_ID ] = "key", \
[ KVS_DEVICE_PUBKEY_ID ] = "pub", \
[ KVS_ROOT_CA_ID ] = "rootca", \
[ KVS_TEMPLATE_NAME ] = "template", \
[ KVS_CLAIM_CERT_ID ] = "claimcert", \
[ KVS_CLAIM_PRIVKEY_ID ] = "claimkey", \
[ KVS_CODE_SIGN_CERT_ID ] = "codesigncert", \
[ KVS_UPDATE_DEVICE_CERT_ID ] = "updatecert", \
[ KVS_UPDATE_DEVICE_PRIVKEY_ID ] = "updatekey", \
[ KVS_UPDATE_DEVICE_PUBKEY_ID ] = "updatepub", \
}
各デモタスクを起動するメインタスクを変更する
各デモタスクを起動するメインタスクを変更します。
iot-reference-rx
では必要に応じてFleet Provisioning
デモ起動後、デバイス個別の証明書を取得してから、PubSub
デモおよびOTA
デモを起動します。
今回の変更では、Fleet Provisioning
デモの代わりに証明書の更新
デモを起動することで、デバイス個別の証明書を更新して、PubSub
デモおよびOTA
デモを起動するように仕様を変更しました。
PubSub
デモおよびOTA
デモは更新された証明書を使用してAWSに接続します。
if(ApplicationCounter(Time2Wait))
{
/* Remove CLI task before going to demo. */
/* CLI and Log tasks use common resources but are not exclusively controlled. */
/* For this reason, the CLI task must be deleted before executing the Demo. */
vTaskDelete(xCLIHandle);
if( !Connect2AP())
{
configPRINTF( ( "Cellular init failed" ) );
}
else
{
vTaskDelay(300);
configPRINTF( ( "Initialise the RTOS's TCP/IP stack\n" ) );
configPRINTF( ( "---------STARTING DEMO---------\r\n" ) );
#if (ENABLE_FLEET_PROVISIONING_DEMO == 1)
vStartFleetProvisioningDemo();
#elif (ENABLE_FLEET_PROVISIONING_DEMO == 2)
vStartUpdateCertificateDemo();
#else
xSetMQTTAgentState( MQTT_AGENT_STATE_INITIALIZED );
#endif
vStartMQTTAgent (appmainMQTT_AGENT_TASK_STACK_SIZE, appmainMQTT_AGENT_TASK_PRIORITY);
vStartSimplePubSubDemo ();
#if (ENABLE_OTA_UPDATE_DEMO == 1)
vStartOtaDemo();
#endif
}
}
while( 1 )
{
vTaskSuspend( NULL );
}
更新された証明書を使用するようにMQTT-Agentタスクを変更する
MQTT-Agentタスクで更新された証明書がある場合、それを使用してAWSに接続するよう仕様を変更します。
もし更新された証明書がない場合は、既存の証明書を使用してAWSに接続します。
if ( (xPkcs11Ret == CKR_OK) && ((xClientCertificate == CK_INVALID_HANDLE) || (xPrivateKey == CK_INVALID_HANDLE)) )
{
LogInfo( ( "Using first device certificate and key" ) );
xNetworkCredentials.pClientCertLabel = pkcs11configLABEL_DEVICE_CERTIFICATE_FOR_TLS;
xNetworkCredentials.pPrivateKeyLabel = pkcs11configLABEL_DEVICE_PRIVATE_KEY_FOR_TLS;
}
else if ( (xPkcs11Ret == CKR_OK) && ((xClientCertificate != CK_INVALID_HANDLE) && (xPrivateKey != CK_INVALID_HANDLE)) )
{
LogInfo( ( "Using updated device certificate and key" ) );
xNetworkCredentials.pClientCertLabel = pkcs11configLABEL_UPDATE_DEVICE_CERTIFICATE_FOR_TLS;
xNetworkCredentials.pPrivateKeyLabel = pkcs11configLABEL_UPDATE_DEVICE_PRIVATE_KEY_FOR_TLS;
}
else
{
LogError( ( "Failed to get state." ) );
}
証明書の更新デモの実装
UpdateCertOverMqttDemoExample.c
に証明書の更新
デモタスクを実装します。
どのように上記デモタスクを実装したかは以下の通りです。
デバイス内部で鍵ペアとCSRを生成する
今回のデモはデバイス内部で鍵ペアとCSRを作成します。
その後、CSRのみMQTTメッセージとしてAWSに送信して、AWS Lambdaで送信されたCSRから証明書を作成して、ポリシーにアタッチします。
そのため、デバイス内部で鍵ペアとCSRを作成する処理を実装します。
これらの処理はFleet Provisioning
デモの関数を流用することで簡単に実装することができます。
/* Initialize the PKCS #11 module */
xPkcs11Ret = xInitializePkcs11Session( &xP11Session );
if( xPkcs11Ret != CKR_OK )
{
LogError( ( "Failed to initialize PKCS #11." ) );
xStatus = false;
}
if ( xPkcs11Ret == CKR_OK )
{
xStatus = xGenerateKeyAndCsr( xP11Session,
pkcs11configLABEL_UPDATE_DEVICE_PRIVATE_KEY_FOR_TLS,
pkcs11configLABEL_UPDATE_DEVICE_PUBLIC_KEY_FOR_TLS,
pcCsr,
ucdemoCSR_BUFFER_LENGTH,
&xCsrLength,
pcCSRSubjectName );
if( xStatus == false )
{
LogError( ( "Failed to generate Key and Certificate Signing Request." ) );
}
}
else
{
LogError( ( "Failed to initialize PKCS #11 or get state." ) );
xStatus = false;
}
既存の証明書を使用してAWSに接続する
更新された証明書を受け取るためには、最初に既存の証明書を使用してAWSに接続してCSRを送信する必要があります。
xEstablishMqttSession
関数の引数には既存の証明書ラベルを指定します。
/**** Connect to AWS IoT Core with exist credentials *****/
if( xStatus == true )
{
LogInfo( ( "Establishing MQTT session with exist certificate..." ) );
xStatus = xEstablishMqttSession( &xMqttContext,
&xNetworkContext,
&xBuffer,
prvProvisioningPublishCallback,
pkcs11configLABEL_DEVICE_CERTIFICATE_FOR_TLS,
pkcs11configLABEL_DEVICE_PRIVATE_KEY_FOR_TLS,
gKeyValueStore.table[KVS_CORE_THING_NAME].value);
if( xStatus == false )
{
LogError( ( "Failed to establish MQTT session." ) );
}
else
{
LogInfo( ( "Established connection with exist credentials." ) );
xConnectionEstablished = true;
}
}
鍵ペアの作成およびCSRをAWSに送信する
xGenerateKeyAndCsr
関数を使用して作成したCSRをAWSに送信します。
ただし、作成されたCSRは\n
が存在しています。
念のため、strtok
関数を使用して\n
を\\n
に変更しておきます。
こちらの方がAWS側のMQTTテストクライアント上でのCSRの見栄えが良いです。
/**** Update Certificate from CSR ***************************/
if( xStatus == true )
{
xStatus = prvSubscribeToCsrResponseTopics();
}
if( xStatus == true )
{
token = strtok( pcCsr, "\n");
while (token != NULL)
{
xConvertCsrLength += snprintf( (pcConvertCsr + xConvertCsrLength), ucdemoCSR_BUFFER_LENGTH, "%s\\n", token);
token = strtok(NULL, "\n");
}
xConvertCsrLength = 0;
xPublishMessageLength = ucdemoCSR_BUFFER_LENGTH + 128U;
pcPublishMessage = pvPortMalloc(xPublishMessageLength + 1);
xPubPayloadLength = snprintf( pcPublishMessage, xPublishMessageLength + 1, ucdemoCSR_REQUEST_JSON, pcConvertCsr);
}
if( xStatus == true )
{
/* Publish the CSR to the CreateCertificatefromCsr API. */
xStatus = xPublishToTopic( &xMqttContext,
pcPublishTopic,
xPubOutTopicLength,
( char * ) pcPublishMessage,
xPubPayloadLength );
if( xStatus == false )
{
LogError( ( "Failed to publish to fleet provisioning topic: %.*s.",
pcPublishTopic,
xPubOutTopicLength ) );
}
}
coreJSONを使用して証明書を抽出する
AWS Lambdaで受け取ったCSRから証明書を作成して送信元のデバイスに送り返します。
これはJSON形式で送られてくるため、coreJSONライブラリを使用して証明書データのみを抽出します。
ただし、この証明書データはLF文字が\\n
になっているため、このままではcorePKCS11ライブラリを使用して証明書データを正しくデータフラッシュに書き込めません。
したがって、strtok
関数を使用して\\n
を\n
に変換してからxLoadCertificate
関数を呼び出して証明書データをデータフラッシュに書き込みます。
if( xStatus == true )
{
xjsonresult = JSON_Validate( pucPayloadBuffer, xSubPayloadLength );
if( xjsonresult == JSONSuccess )
{
xjsonresult = JSON_Search( pucPayloadBuffer, xSubPayloadLength, "certificatePem", (sizeof("certificatePem") - 1),
&jsonvalue, &jsonvalueLength );
if ( xjsonresult == JSONSuccess )
{
pcCert = pvPortMalloc(jsonvalueLength + 1);
memcpy(pcCert, jsonvalue, jsonvalueLength);
pcConvertCert = pvPortMalloc(jsonvalueLength + 1);
token = strtok(pcCert, "\\");
while (token != NULL)
{
if (token == pcCert)
{
xConvertCertLength += snprintf( (pcConvertCert + xConvertCertLength), jsonvalueLength, "%s\n", token);
}
else
{
xConvertCertLength += snprintf( (pcConvertCert + xConvertCertLength), jsonvalueLength, "%s\n", (token + 1));
}
token = strtok(NULL, "\\");
}
xStatus = true;
}
else
{
LogError( ( "Failed to dumps certificatePem." ) );
xStatus = false;
}
}
else
{
LogError( ( "Failed to dumps certificatePem." ) );
xStatus = false;
}
}
if( xStatus == true )
{
/* Save the certificate into PKCS #11. */
xStatus = xLoadCertificate( xP11Session,
pcConvertCert,
pkcs11configLABEL_UPDATE_DEVICE_CERTIFICATE_FOR_TLS,
xConvertCertLength );
}
更新された証明書を使用して接続確認する
一度AWSから切断した後に更新された証明書を使用してAWSへの接続を確認します。
xEstablishMqttSession
関数の引数には更新された証明書ラベルを指定します。
/**** Connect to AWS IoT Core with updated certificate ************/
if( xStatus == true )
{
LogInfo( ( "Establishing MQTT session with updated certificate..." ) );
xStatus = xEstablishMqttSession( &xMqttContext,
&xNetworkContext,
&xBuffer,
prvProvisioningPublishCallback,
pkcs11configLABEL_UPDATE_DEVICE_CERTIFICATE_FOR_TLS,
pkcs11configLABEL_UPDATE_DEVICE_PRIVATE_KEY_FOR_TLS,
gKeyValueStore.table[KVS_CORE_THING_NAME].value );
if( xStatus != true )
{
LogError( ( "Failed to establish MQTT session with provisioned "
"credentials. Verify on your AWS account that the "
"new certificate is active and has an attached IoT "
"Policy that allows the \"iot:Connect\" action." ) );
}
else
{
LogInfo( ( "Sucessfully established connection with provisioned credentials." ) );
xConnectionEstablished = true;
}
}
デモの動作確認手順
モノの作成および初回の証明書/秘密鍵のダウンロード
AWSにCSRを送信するために初回接続に使用する証明書/秘密鍵をダウンロードします。
下記iot-reference-rx
におけるGetting_Started_Guide.md
のStep 4-1: Run PubSub demo
記載の方法に従って、モノの作成および証明書/秘密鍵をダウンロードしてください。
証明書を作成して送り返すAWS Lambda関数の作成
デバイスからMQTTメッセージとして送信されたCSRを用いて証明書を作成してから、デバイスに送り返すLambda関数を以下のように作成します (使用言語はPython 3.12を使用しています)。
boto3
ライブラリを使用して、受け取ったCSRから証明書を作成、それをポリシーおよびモノに割り当てています。
最後に作成した証明書をMQTTメッセージとしてデバイスに送り返しています。
ck_rx65n_test_policy
は作成したポリシー名に合わせて変更してください。
import json
import boto3
import base64
def lambda_handler(event, context):
clientid = event['clientid']
CSR = event['CSR']
print(CSR)
client = boto3.client('iot')
iot = boto3.client('iot-data')
response = client.create_certificate_from_csr(
certificateSigningRequest = CSR,
setAsActive = True
)
certificatePem = response['certificatePem']
certificateArn = response['certificateArn']
client.attach_policy(
policyName = 'ck_rx65n_test_policy',
target = certificateArn
)
response = client.attach_thing_principal(
thingName = clientid,
principal = certificateArn
)
topic = 'management/topic/%s/crt' % clientid
payload = {
"certificatePem": certificatePem
}
iot.publish(
topic=topic,
qos=0,
payload=json.dumps(payload)
)
return {
'statusCode': 200,
'certificatePem': certificatePem
}
また今回はAWS Lambda関数上でAWS IoT Coreのサービスを使用するため、AWS Lambda関数の設定
-> アクセス権限
-> 実行ロール
からAWS IoT Core
のロールを付与しておきます。
とりあえず、実験としてはAWSIoTFullAccess
で良いと思います。
AWS IoT CoreのRuleの作成
AWS IoT CoreとAWS Lambdaを繋ぐためのRuleを作成します。
下記のようにSQLステートメントを設定して、トピック名がmanagement/topic/<clientid>/csr_res
である場合、先ほど作成したAWS Lambda関数にMQTTメッセージを転送するように設定します。
SELECT clientid() as clientid, * FROM 'management/topic/+/csr_res'
プログラムの実行
v202210.01-lts-rx-1.1.2-update-cert
ブランチからプロジェクトおよびソースコードをクローンしてから、e2 studioにaws_ryz014a_ck_rx65n
をインポートしてください。
使用するSIMカードに応じて、Cellular用の情報を設定してプログラムをビルドします。
その後、プログラムを実行して、下記step-4-1-6-input-via-cli-for-pubsub-demo
に従って、TeraTermでモノの名前
、エンドポイント名
、証明書
、秘密鍵
を入力してリセットします。
リセット後、数秒間経過するとデモが実行されます。
最初に証明書の更新タスクが実行されて、デバイス内部で作成したCSRをAWSに送信します。
その後、AWSから送り返された証明書をcorePKCS11ライブラリを使用してデータフラッシュに書き込みます。
TeraTermのログにDemo completed successfully.
が表示されれば証明書の更新タスクの実行は成功です。
その後、MQTT-AgentタスクとPubSubタスクが実行されます。
これらのタスクにおけるAWSとの接続には更新された証明書が使用されます。
Using updated device certificate and key
が表示されれば、更新された証明書が接続に使用されていることを意味します。
AWS IoT Coreのポリシーを確認すると、CSRから作成された証明書がポリシーにアタッチされていることを確認できます。
さいごに
今回はAWSで公開されている証明書更新アプローチをRXファミリ用のFreeRTOS-LTSで実装してみました。
今回の実装では、SNSでスケジュールを組んでAWS Lambdaで証明書の有効期限を定期的に確認するまでの実装は行いませんでしたが、この実装を参考にAWSで公開されている証明書更新アプローチを実装することできるはずです。
また、デモでは既存の証明書をデバイス側でもAWS側でも削除していません。
そのため、必要に応じてデバイス側はプログラムでAWS側はAWS Lambda関数で既存の証明書を削除する処理を実装する必要があります。
さらに今回の実装ではFleet Provisioning
デモと証明書の更新
デモは同時に動かすことができません。
ユーザーの中にはFleet Provisioning
デモで取得したデバイス個別の証明書を証明書の更新
デモで更新したい人がいるかもしれません。
そのためには、証明書の更新
デモをPubSub
デモとOTA
デモと同じタイミングで動かして、AWS Lambda側でPublishしたMQTTメッセージをトリガーにCSRの作成および証明書の更新処理を実行する必要があります。
証明書更新中はPubSub
デモとOTA
デモを動かさずに、証明書更新後にAWSに再接続してPubSub
デモとOTA
デモを再開するようにサスペンドしておくとよいでしょう。