セキュリティ
デバッガーが嘘をつくとき
When the Debugger Lies (danielmangum.com)
要約
この記事は、Nordic SemiconductorのnRF54Lシリーズにおけるキー管理ユニット(KMU)のデバッグ中に発生した予期せぬメモリ値の問題を解説しています。デバッガーが示す値と実際のシステム内部の動作との間に乖離が生じる原因は、SoCコンポーネントとデバッガーの相互作用の理解不足にあることを指摘しています。筆者は、手動でレジスタを操作するデバッグ手法を通じて、KMUのプロビジョニングとプッシュタスクの実行プロセスを詳細に調査し、その挙動を明らかにしています。
全文翻訳
私はここ数週間、Nordic SemiconductorのnRF54Lシリーズのセキュリティアーキテクチャに取り組んできました(もし見逃していたなら、私は最近Nordicに入社しました!)。その過程で、デバッガーを使ってレジスタを手動で操作するという、私のいつもの低レベル学習テクニックを採用しました。数晩前、キー管理ユニット(KMU)を操作中に、メモリ内の予期しない値に気づきました。それはよくある問題でしたが、システムオンチップ(SoC)の内部コンポーネントと、デバッガーがそれらとどのように相互作用するかを理解しないと診断できないものでした。
背景として、nRF54Lシリーズは、Cortex-M33コアのArm TrustZoneサポート、CRACEN暗号アクセラレータ、およびキー管理ユニット(KMU)を特徴とする、かなり高度なセキュリティ機能セットを備えています。KMUは、キーシードや関連メタデータなどの機密データをセキュア情報構成領域(SICR)に保存するために使用されます。SICRはスロットに分割されています。これらのスロットは、セキュアモードでのみアクセス可能なKMUにタスクを発行することによってターゲットにされます。通常、アプリケーションファームウェアはKMUと直接対話しません。代わりに、PSAドライバーが実装され、キーの生成、保存、および使用を抽象化します。たとえば、psa_generate_key()を呼び出すと、その操作は最終的にCRACEN PSAドライバーのimport_key_for_kmu()への呼び出しにつながります。
static psa_status_t import_key_for_kmu(const psa_key_attributes_t *attributes, const uint8_t *data, size_t data_length, uint8_t *key_buffer, size_t key_buffer_size, size_t *key_buffer_length, size_t *key_bits) {
size_t opaque_key_size;
psa_status_t status = PSA_ERROR_CORRUPTION_DETECTED;
int slot_id = CRACEN_PSA_GET_KMU_SLOT(MBEDTLS_SVC_KEY_ID_GET_KEY_ID(psa_get_key_id(attributes)));
psa_key_attributes_t stored_attributes;
status = cracen_get_opaque_size(attributes, &opaque_key_size);
if (status != PSA_SUCCESS) {
return status;
}
if (key_buffer_size < opaque_key_size) {
return PSA_ERROR_BUFFER_TOO_SMALL;
}
status = cracen_kmu_provision(attributes, slot_id, data, data_length);
if (status != PSA_SUCCESS) {
return status;
}
status = cracen_kmu_get_builtin_key(slot_id, &stored_attributes, key_buffer, key_buffer_size, key_buffer_length);
if (status != PSA_SUCCESS) {
return status;
}
*key_bits = psa_get_key_bits(&stored_attributes);
return status;
}
スロットは、消去、プロビジョニング、または取り消しが可能です。データシートには、ステートマシンの便利な図が含まれています。スロットにはID(0~249)があり、メタデータ(32ビット)、宛先アドレス(32ビット)、値(128ビット)、および取り消しポリシー(2ビット)を格納できます。後者は、キーがプロビジョニングされた状態にあるときに状態がどのように変化するか、およびそのスロットIDを参照するさまざまなタスクがKMUに発行されたかを決定します。
キーがプロビジョニングされる際、スロットは消去、プロビジョニング、または取り消しが可能です。データシートにはステートマシンの図が役立ちます。スロットにはID(0~249)があり、メタデータ(32ビット)、宛先アドレス(32ビット)、値(128ビット)、および取り消しポリシー(2ビット)を格納できます。後者は、キーがプロビジョニングされた状態にあるときに状態がどのように変化するか、およびそのスロットIDを参照するさまざまなタスクがKMUに発行されたかを決定します。通常、アプリケーションファームウェアはKMUと直接対話しません。代わりに、PSAドライバーが実装され、キーの生成、保存、および使用を抽象化します。たとえば、psa_generate_key()を呼び出すと、その操作は最終的にCRACEN PSAドライバーのimport_key_for_kmu()への呼び出しにつながります。
static int cracen_kmu_key_slot_provision(const nrfx_kmu_key_slot_data_t *key_slot_data, uint32_t slot_id) {
int kmu_status;
uint8_t orig_write_buf_size;
cracen_kmu_key_slot_provision_write_enable_set(true, &orig_write_buf_size);
kmu_status = nrfx_kmu_key_slot_provision(key_slot_data, slot_id);
cracen_kmu_key_slot_provision_write_enable_set(false, &orig_write_buf_size);
return kmu_status;
}
nrfx_kmu_key_slot_data_tの定義は、Nordic Zephyrハードウェア抽象化レイヤー(HAL)で見つけることができます。
typedef struct __PACKED {
uint32_t keyslot_value[KEY_SLOT_WORDS_COUNT]; ///< プロビジョニングするキーデータ。
#if NRF_KMU_HAS_REVOKE_POLICY || defined(__NRFX_DOXYGEN__)
uint32_t revoke_policy; ///< キー取り消しポリシー。
/// @ref nrfx_kmu_rpolicy_t * が可能な値を保持します。
#endif
uint32_t keyslot_dest; ///< キープッシュ実行時のキー スロット宛先。
#if NRF_KMU_HAS_METADATA || defined(__NRFX_DOXYGEN__)
nrfx_kmu_key_slot_metadata_t metadata; ///< キー スロットに書き込むメタデータ。
#endif
} nrfx_kmu_key_slot_data_t;
かなり簡単なファームウェアを作成したり、CRACEN KMUサンプルを使用してビルドし、開発キットにフラッシュしてKMUにキーを簡単にプロビジョニングしたりできます。しかし、サポートされているドライバー(絶対に使うべきです)を使用する場合、キー スロットに値をプロビジョニングする際に、追加の制限が課せられます。たとえば、KMUはメタデータに任意の32ビット値をサポートしますが、PSAドライバーは各ビットに意味を割り当てます。
typedef struct kmu_metadata {
uint32_t metadata_version: 4;
uint32_t key_usage_scheme: 2;
uint32_t reserved: 8;
uint32_t algorithm: 6;
uint32_t size: 3;
uint32_t rpolicy: 2;
uint32_t usage_flags: 7;
} kmu_metadata;
同様に、書き込める値や、特定の種類のキーがプッシュされる宛先にも制限があります。KMUと手動で対話する場合、メタデータ、値、および宛先ははるかに柔軟です。しかし、KMUのスロットに非準拠のデータをプロビジョニングしてから、サポートされているドライバーを使用して対話しようとすると、うまくいきません。
リスクを認識し、CTRL-AP(コントロールアクセスポート)のERASEALL操作でnRF54LM20 DKのSICRを復元できることを知っていたので、ボードの電源を入れ、GDBで接続しました。前述のように、KMUはセキュアモードでのみアクセスできます。しかし、アクセスポート保護が有効になっていない場合、Secure Privileged Invasive Debug Enable(SPIDEN)信号が高になり、オンボードJ-Linkデバッガー(J-Link OB)はセキュア権限で動作できます。CPUが停止した状態で、KMUにアクセスできることをテストし、STATUSレジスタ(0x50049400)を読み取ることで、操作の準備ができていることを確認しました。
(gdb) x/1xw 0x50049400
0x50049400: 0x00000000
実際の機能をテストするために、データシートに記載されているプロビジョニングとプッシュの手順に従いました。最初のステップは、メモリ内にSRCデータ構造を構築することでした。これは、パックされたnrfx_kmu_key_slot_data_t構造体定義で指定された形式です。複数の値を簡単にテストできるように、構造体を構築するための小さなPythonスクリプトを作成しました。
import struct
open("kmu_src.bin", "wb").write(
bytes.fromhex("abc123abc123abc123abc123abc123ab") # 値
+ struct.pack(
"<III",
3, # 取り消しポリシー
0x20000000, # 宛先アドレス
0xDEF678DE, # メタデータ
)
)
生成されたkmu_src.binには以下の内容が含まれていました。
xxd kmu_src.bin
00000000: abc1 23ab c123 abc1 23ab c123 abc1 23ab ..#..#..#..#..#.
00000010: 0300 0000 0000 0020 de78 f6de ....... .x..
構造体のアドレス(0x20001000)をKMU SRCレジスタ(0x50049504)に書き込み、KEY_SLOTレジスタ(0x50049500)で目的のキー スロット(9)を指定するために、GDBのrestoreコマンドを使用しました。
(gdb) restore kmu_src.bin binary 0x20001000
(gdb) set *(unsigned int*)(0x50049504) = 0x20001000
(gdb) set *(unsigned int*)(0x50049500) = 9
タスクを発行する前に、抵抗性ランダムアクセスメモリコントローラー(RRAMC)を設定して、バッファリングされていない書き込みを許可する必要があります。タスク完了後に復元するために、現在のRRAMC CONFIGレジスタ(0x5004e500)を保存し、値1を書き込みました。これにより、書き込み許可(WEN)フィールドが1に、バッファサイズ(WRITEBUFSIZE)が0(バッファリングなし)に設定されます。
(gdb) set $rramc_config = *(unsigned int*)(0x5004e500)
(gdb) set *(unsigned int*)(0x5004e500) = 1
最後に、TASKS_PROVISIONレジスタ(0x50049000)に1を書き込み、KMUに値とそのメタデータをキー スロット9に保存するように指示しました。その後、EVENTS_PROVISIONEDレジスタ(0x50049100)を読み取ることで、イベントが生成されたことを確認しました。
(gdb) set *(unsigned int*)(0x50049000) = 1
(gdb) x/1wx 0x50049100
0x50049100: 0x00000001
タスクが完了した後、RRAMC CONFIGレジスタをリセットしました。
(gdb) s