[reTerminal] PKCS#11を用いて秘密鍵を安全に管理しつつAWS IoTとMQTT通信する

2022.05.06

はじめに

マルツオンライン様のGWセールでreTeminalが特価でしたので、購入してみました。reTeminalの詳細は、以下をご参照ください。

reTerminalには、ハードウェアベースの安全なキー・ストレージが付いています。こちらを利用したブログネタを探していたところ、aws-iot-device-sdk-python-v2でPKCS#11のサポートがされていましたので、試すことにしました。

PKCS#11とは

PKCS(Public Key Cryptography Standards)は、RSAセキュリティにより考案され公開された公開鍵暗号標準です。中でもPKCS#11は、HSM(Hardware Security Module)のためのAPIとなります。 IoTでは、認証情報を不正に取得されないように、認証情報をTPM(Trusted Platform Module)やHardware Security Module(HSM)といったハードウェア保護モジュールを利用することがあります。

冒頭で述べた通り、reTerminalのATECC608Aが、このハードウェア保護モジュールにあたります。本記事では、各種ツールのPKCS#11のAPIを利用して、秘密鍵を安全にデバイス側で格納しつつAWS IoTとMQTT通信を実現します。

環境

reTerminalのOSはRaspbianです。ただ、Raspberry Piを持っている方が本記事をそのまま利用することはできません。参考程度にとどめてください。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 10 (buster)
Release:        10
Codename:       buster
$ openssl version
OpenSSL 1.1.1n  15 Mar 2022

reTerminalの初期設定

起動からwifi設定

電源をつけて、wifi設定まで行ってください。Zennでスクラップを書いたので、以下をご参照ください。

ATECC608Aのプロビジョニング

seeed様の記事、項目「デバイス秘密鍵とデバイス公開鍵を確認する」まで進めてください。

以下のshared objectファイルが存在するか確認します。後述するopensslの設定で利用します。(もしかしたら環境差異があるかもしれません)

  • lib11 -> /usr/lib/arm-linux-gnueabihf/engines-1.1/libpkcs11.so
  • cryptoauthlib -> /usr/lib/libcryptoauth.so

p11toolで、以下のコマンドを実行し、Object 0にPrivate key、Object 1にPublic keyが割り当てられているのが確認できましたら、次の項目に進みます。

$ p11tool --provider /usr/lib/libcryptoauth.so --list-all
$ p11tool --list-all "pkcs11:token=MCHP"

(余談) 紹介した記事にもある通り、reTerminalのATECC608Aは未プロビジョニング品となっています。私は業務でプロビジョニング済みの製品を利用していたため、R/W可能なスロットにデータが書き込めず混乱しました。結果この記事の存在に気付くことなく、自己流でプロビジョニングしました。。プロビジョニング状態の確認は、記事の項「ロック状態を確認」を参考にしてください。

MQTT接続準備

cryptoauthlibの再ビルド

項「ATECC608Aのプロビジョニング」で実施したcryptoauthlibをビルドしなおします。v3.3.3(リビジョン:055dd4af)の場合、MQTT通信時以下のようなエラーが発生するためです。原因については分かり次第、追記したいと思います。

Loading PKCS#11 library '/usr/lib/libcryptoauth.so' ...
Loaded!
Traceback (most recent call last):
  File "pkcs.py", line 39, in <module>
    connect_future.result()
  File "/usr/lib/python3.7/concurrent/futures/_base.py", line 432, in result
    return self.__get_result()
  File "/usr/lib/python3.7/concurrent/futures/_base.py", line 384, in __get_result
    raise self._exception
awscrt.exceptions.AwsCrtError: AWS_ERROR_PKCS11_ENCODING_ERROR: A PKCS#11 (Cryptoki) library function was unable to ASN.1 (DER) encode a data structure. See log for more details.

筆者の環境では、リビジョン:9a37b8d6では動作したため、こちらを利用します。前の作業で利用したものを再利用しても良いですが、本手順ではクローンし直して、再ビルドします。

$ git clone https://github.com/MicrochipTech/cryptoauthlib
$ cd cryptoauthlib
$ git reset --hard 9a37b8d6
$ mkdir build
$ cd build
$ cmake \
  -DATCA_HAL_I2C=ON \
  -DATCA_PKCS11=ON \
  -DATCA_TNGTLS_SUPPORT=ON \
  -DATCA_ATECC608A_SUPPORT=ON \
  -DATCA_OPENSSL=ON ../
$ cmake --build .
$ sudo make install

以下のコマンドでファイルの生成時刻を確認し、再度インストールされていることを確認します。

$ ls -al /usr/lib/ | grep "libcryptoauth.so"

opensslの設定

MicrochipTech様のPKCS11 Linux Setup|Without using p11-kit-proxyを参考にしました。 OpenSSLがlibp11cryptoauthlibを使用するように手動で設定します。前述したパスをopenssl.cnfに追記します。openssl_initは先頭に記述する必要があるため注意してください。

デフォルトのコンフィグとのdiffは以下の通りです。

$ diff /usr/lib/ssl/openssl.cnf /usr/lib/ssl/openssl.cnf_bakup
1,14d0
< openssl_conf = openssl_init
< [openssl_init]
< engines=engine_section
< 
< [engine_section]
< pkcs11 = pkcs11_section
< 
< [pkcs11_section]
< engine_id = pkcs11
< # Wherever the engine installed by libp11 is. For example it could be:
< # dynamic_path = /usr/lib/ssl/engines/libpkcs11.so 
< dynamic_path = /usr/lib/arm-linux-gnueabihf/engines-1.1/libpkcs11.so
< MODULE_PATH = /usr/lib/libcryptoauth.so
< init = 0

CSRの作成

CSRを作成します。よくある手順だと、秘密鍵のファイルを指定しますが、ATECC608Aは秘密鍵を読み出せないため、PKCS#11のツールを利用します。

sudo openssl req -engine pkcs11 -key "pkcs11:token=MCHP;object=device;type=private" -keyform engine -new -out new_device.csr -subj "/C=JP/ST=Tokyo/L=Tokyo/O=MyCompany/CN=12345"

以下のコマンドで、CSRの内容を確認できます。

$ openssl req -in new_device.csr -text
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C = JP, ST = Tokyo, L = Tokyo, O = MyCompany, CN = 12345
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                ...
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        Attributes:
            a0:00
    Signature Algorithm: ecdsa-with-SHA256
    ...
-----BEGIN CERTIFICATE REQUEST-----
...
-----END CERTIFICATE REQUEST-----

証明書の作成

AWS IoTでは、CSRから証明書を作成する方法があります。今回はそちらを活用します。

マネジメントコンソールで行う場合 CSRによる証明書の作成を参考にしてください。

AWS CLIで行う場合

# 登録したいAWS環境にassume roleする
aws iot create-certificate-from-csr \
    --certificate-signing-request=file://new_device.csr \
    --set-as-active
{
    "certificateArn": "arn:aws:iot:ap-northeast-1:111111111111:cert/<certificateId>",
    "certificateId": "<certificateId>",
    "certificatePem": "-----BEGIN CERTIFICATE-----***-----END CERTIFICATE-----\n"
}

JSONの証明書部分(certificatePem)を、device.crtとして保存しておきます。

# 制御文字を反映させるため-eオプションをつけます。
echo -e "-----BEGIN CERTIFICATE-----***-----END CERTIFICATE-----\n">device.cert

証明書にポリシーをアタッチ

ポリシー定義

echo '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow","Action":["iot:*"],"Resource": ["*"]}]}'|jq>policy.json

ポリシー作成

aws iot create-policy --policy-name reteminal-test \
  --policy-document file://policy.json

実行結果

{
    "policyName": "reteminal-test",
    "policyArn": "arn:aws:iot:ap-northeast-1:111111111111:policy/reteminal-test",
    "policyDocument": "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"iot:*\"\n      ],\n      \"Resource\": [\n        \"*\"\n      ]\n    }\n  ]\n}\n",
    "policyVersionId": "1"
}

証明書にポリシーをアタッチ。(何も出力されなければ成功です)

aws iot attach-policy \
--policy-name reteminal-test \
--target arn:aws:iot:ap-northeast-1:111111111111:cert/<certificateId>

最終的に以下のようになっていることを確認します。 img

MQTT通信確認

MQTT通信するために必要なsdkのインストール、通信時に利用するルートCAを取得します。

# awsiotsdkのインストール
pip3 install awsiotsdk
# ルートCAのダウンロード
wget https://www.amazontrust.com/repository/AmazonRootCA1.pem

以下のような配置にします

.
├── AmazonRootCA1.pem # wgetで取得
├── pkcs.py # 後述するpythonスクリプト
└── device.crt # 証明書作成の項目で作成

ソースコードは以下となります。ポイントは、通常秘密鍵を平文で設定していたところを、本コードでは設定していないことです。前述しましたがATECC608AのSLOT0番は、公開鍵は取得出来ても、秘密鍵の書き込み、読み出しは不可能です。故に安全に秘密鍵を管理することが可能です。

pkcs.py

from awsiot import mqtt_connection_builder
from awscrt import io, http, auth, mqtt
import time
import json

def on_connection_interrupted(connection, error, **kwargs):
    print("Connection interrupted. error: {}".format(error))

def on_connection_resumed(connection, return_code, session_present, **kwargs):
    print("Connection resumed. return_code: {} session_present: {}".format(return_code, session_present))

if __name__ == '__main__':
    pkcs11_lib_path = "/usr/lib/libcryptoauth.so"

    print(f"Loading PKCS#11 library '{pkcs11_lib_path}' ...")
    pkcs11_lib = io.Pkcs11Lib(
        file=pkcs11_lib_path,
        behavior=io.Pkcs11Lib.InitializeFinalizeBehavior.STRICT)
    print("Loaded!")

    mqtt_connection = mqtt_connection_builder.mtls_with_pkcs11(
        pkcs11_lib=pkcs11_lib, # cryptoauthlibのコンパイル済みshared objectファイルを指定
        user_pin="1234", # 適当
        token_label="MCHP", # /var/lib/cryptoauthlib/0.confのlabel値を指定
        private_key_label="device", # /var/lib/cryptoauthlib/0.confから指定
        cert_filepath="./device.crt", # 項「証明書の作成」で作成した証明書
        endpoint="***-ats.iot.ap-northeast-1.amazonaws.com", # AWS IoT Coreエンドポイント
        port=8883,
        ca_filepath="./AmazonRootCA1.pem", # wgetで取得
        on_connection_interrupted=on_connection_interrupted, # 接続時コールバック
        on_connection_resumed=on_connection_resumed, # 切断時コールバック
        client_id="myid", # モノに一意は値を指定
        clean_session=False,
        keep_alive_secs=30
    )

    connect_future = mqtt_connection.connect()
    connect_future.result()
    print("Connected!")

    while True:
        PUBLISH_TOPIC_NAME = "sample/test"
        mqtt_connection.publish(
            topic=PUBLISH_TOPIC_NAME,
            payload=json.dumps({"hoo": "hoge"}),
            qos=mqtt.QoS.AT_LEAST_ONCE,
        )

        time.sleep(5)
$ python3 pkcs.py
Loading PKCS#11 library '/usr/lib/libcryptoauth.so' ...
Loaded!
Connected!

publish出来ていることが確認できたらOKです。 img

最後に

今回はスロット0で試しましたが、スロット2,3,4では、フリーの秘密鍵を設定可能です。フリーの秘密鍵を利用するパターンでも試してみようと思います。恐らく/var/lib/cryptoauthlib/0.confの設定を変更すればいけると思います。。

参考文献