はじめに
IoT機器などでTLSの相互認証(クライアント証明書を使う認証)を行う場合、mbedTLSでは以下の手順で設定が必要です。
-
mbedtls_ssl_conf_own_cert
でクライアント証明書をセット -
mbedtls_pk_parse_key
で秘密鍵をセット
上記の方法だと、実行ファイルやファームウェアイメージのどこかに秘密鍵をPEM形式かDER方式で保存しておく必要があり、秘密鍵の漏洩リスクがあります。
秘密鍵をセキュアエレメントやMPUのセキュアストレージに保管すればこの問題は解決しますが、mbedTLSにどう組み込むか... というのがなかなか大変でした。
この記事は、これを何とかした(そして思いのほか紆余曲折があった)という内容です。
前提
TLS 1.2とTLS 1.3両方をサポートしたいという要求があります。
また、TLSの鍵合意はフォワードセキュリティのためECDHEを使います。そのためクライアント証明書はCertificateVerifyの生成(ハッシュへの署名)のみに用い、暗号化のためには使いません。
ここでは、秘密鍵の漏洩を防ぐためのみセキュアエレメントを使い、ECDHやAES、SHAなどの処理はソフトウェアベースのままです。
PKCS#11
概要
FreeRTOSのnetwork_transport実装には、秘密鍵を直接ロードするのではなく、PKCS#11 APIで管理するコードがあります(transport_mbedtls_pkcs11.[c|h]
)。
このコードは、mbedtls_pk_pkcs11.[c|h]
にmbedtls_pk_context
のカスタム実装を持っていて、秘密鍵を用いた操作(署名とか検証)が必要になったときにPKCS#11 APIを呼び出すように実装されています。
呼び出されるPKCS#11 APIは、corePKCS11がmbedTLSを利用して実装しています。
問題点
mbedTLS 3.2.1 で RSA鍵の場合、かつTLS 1.3の場合はこのコードは動きませんでした(最近の実装だと動くのかもしれませんが)。
TLS 1.3でのCertificateVerifyに必要な署名は、RSA-PKCSではなくRSA-PSSであり、RSA-PSSは mbedTLS内でPSAを使って実装されているためフックが呼ばれないようです。
また、PKCS#11 APIは、正しく使うのが面倒です..
さらに、独自のmbedtls_pk_context実装やPKCS#11実装のため、本来mbedTLSに投げることのできる色々なコードが独自実装されており、車輪の再発明感がすごいです(注:個人の感想です)。ECC/RSAとも、独自実装のためビット数に制限があります。
MBEDTLS_ECDSA_SIGN_ALT
概要
mbedTLS で署名生成を置き換える古くからの方法として、MBEDTLS_ECDSA_SIGN_ALTマクロをmbedtls_config.h
で定義するという方法があります。mbedtls_ecdsa_sign
関数を置き換えることで、セキュアエレメントに署名処理を投げることができます。
この方法は比較的メジャーな方法のようで、セキュアエレメントATECC608を利用するためのcryptoauthlibにも実装が存在しています。
問題点
この方法だと、署名関数の実装を複数持つことが難しいです。ソフトウェア実装もセキュアエレメントも使いたい...ということが、この方法だとできません(または難しい)。
また、mbedTLSの内部と緊密に結びつくことになり、例えばcryptoauthlibの実装はmbedTLS 3.2.1では動きませんでした(置き換えの関数を公開してくださっている方がいらっしゃいます)。
RSA秘密鍵の場合は何を差し替えるのか、という問題もあります(差し替え方法を調べきれていません)。
PSA driver
現時点では、PSA (Platform Security Archtecture) で、opaque key(キーの内部についてPSAが関知しないもの)を扱うドライバを書くことがベストな選択のようです。
mbedTLSの設定
PSAを有効にするため以下のように設定します。
#define MBEDTLS_PSA_CRYPTO_C
#define MBEDTLS_USE_PSA_CRYPTO
#define MBEDTLS_PSA_CRYPTO_DRIVERS
この設定を行ったのち、psa_crypto_driver_wrapper.h
で宣言されている各関数をフックすることで、セキュアエレメントを利用するように設定できます。
例えば、秘密鍵を用いてCertificateVerifyを行うために必要なのはsign_hash操作なので、psa_driver_wrapper_sign_hash
をフックします。
また、メモリ上でのopaque keyのサイズをPSAに知らせるため、psa_driver_wrapper_get_key_buffer_size
またはpsa_driver_wrapper_get_key_buffer_size_from_key_data
もフックします。
関数のフックは、同じシグネチャのドライバ関数を書き、フック対象の関数内に呼び出しコードを書くことで行います。mbedTLSのscripts/フォルダにjson形式のファイルからコードを自動生成するスクリプトがありますが、自動生成可能な関数が少なく、現状では手動でのフックが良いです。
ATECC608のようなセキュアエレメントで、内蔵のキー数が決まっている場合、以下の設定も行います。
#define MBEDTLS_PSA_CRYPTO_BUILTIN_KEYS
この設定を行うと、mbedTLSはユーザ定義の以下の関数を呼び出して事前に定義したキーを取得できるようになります。
ユーザ側では、psa_open_key
を呼び出すことで事前に定義したキーを利用します。
※ 2024/8/26追記: psa_open_keyを呼び出さずに,あたかもpsa_import_key()したかのように事前定義キーを利用することができ,こちらが推奨されています(openless crypto API)。
psa_status_t mbedtls_psa_platform_get_builtin_key(
mbedtls_svc_key_id_t key_id,
psa_key_lifetime_t *lifetime,
psa_drv_slot_number_t *slot_number);
一方、Renesas TSIP のように、メモリ上に用意した鍵生成情報を使って任意個数の鍵を扱えるものの場合、ユーザ側はpsa_import_key
を呼び出すことで鍵生成情報からPSAのキーを生成します。
この時、ドライバ側ではpsa_driver_wrapper_import_key
をフックしておく必要があります。
keyのlocationとpersistence、lifetime
PSAにおいて、キーがどこに格納されるかを示すものがlocationです。
ソフトウェア実装の場合(transparentな場合)、locationはPSA_KEY_LOCATION_LOCAL_STORAGE
になります。
PSA driver でopaqueなキーの場合、独自のlocationを決める必要があります。
PSAにおいては、キーのpersistenceも決めておく必要があります。
例えば、Renesas TSIPにおいて鍵生成情報に基づいてimport_keyした鍵は、電源を切ると消えるため、PSA_KEY_PERSISTENCE_VOLATILE
ですが、ATECC608のキーは不揮発性で読み取り専用のためPSA_KEY_PERSISTENCE_READ_ONLY
です。
locationとpersitenceを合成したものがキーのlifetimeです。
これを行うのがPSA_KEY_LIFETIME_FROM_PERSISTENCE_AND_LOCATION
マクロで、PSA driver でキーのlifetimeを定義するときに利用します。
psa_driver_wrapperにはkeyのlocationによるswitch文があるので、以下の手順でPSA driver を実装・利用するように設定することができます。
- location/lifetimeを定義
- フックしたい関数と同シグネチャの関数を実装
- psa_driver_wrapper から実装した関数を呼び出し
制限事項
現在のmbedTLSの実装(~3.5.1)では、鍵合意後の共有秘密を取り出せる必要があります。
(例:TLS 1.2だとssl_write_client_key_exchange
でpsa_raw_key_agreement
を呼んでいる)
Renesas TSIPのように暗号鍵の平文を入力できない仕様の場合、その暗号コプロセッサは以後の処理では利用できないことになります。
TSIPで暗号のオフロードも行いたい場合は、mbedTLSをベンダが改変したものを使うことになるはずです。