PSA APIs 02 · API の共通作法 — 戻り値・初期化・バッファの流儀

Chapter 02

API の共通作法 — 戻り値・初期化・バッファの流儀

この章のゴール.

どの PSA 関数にも共通する 5 つの決まりごと—— 戻り値、初期化、定数の作り方、出力バッファの渡し方、操作オブジェクトの寿命—— を覚え、初めて見る関数でも引数の意味が推測できるようになること。

この章で使う既出の用語(定義は各リンク先). PSA(01 章 1 節)

1. すべての関数は psa_status_t を返す

PSA API の関数は、ほぼ例外なく psa_status_t(32 ビット符号付き整数)を返す。 成功は PSA_SUCCESS(値 0)、失敗は負の値である。 計算結果は戻り値ではなく、引数として渡したポインタの先に書き込まれる。

psa_status_t st = psa_hash_compute(PSA_ALG_SHA_256, in, in_len, out, sizeof out, &out_len);
if (st != PSA_SUCCESS) {
    /* 失敗。out の中身は不定。out_len も信用しない */
}

エラーコードは psa/error.h で定義され、全 API で共通である(Crypto でもストレージでも同じ番号)。 よく見る値を、実際に遭遇する順に並べる。

定数値意味初心者が踏む典型的な原因
PSA_SUCCESS0成功—
PSA_ERROR_BAD_STATE−137呼ぶ順番が違うpsa_crypto_init() を忘れた。分割操作で setup の前に update を呼んだ
PSA_ERROR_NOT_SUPPORTED−134この実装はそのアルゴリズム・鍵種別に対応していないビルド設定で無効。PSA_WANT_ALG_* を確認(15 章)
PSA_ERROR_NOT_PERMITTED−133鍵の用途(usage)や方針が許していない鍵属性に PSA_KEY_USAGE_SIGN_HASH を付け忘れた。書き込み専用ストレージに再書き込み
PSA_ERROR_INVALID_ARGUMENT−135引数の組み合わせが不正鍵種別とアルゴリズムが合わない。鍵長が不正。ハッシュ長が違う
PSA_ERROR_BUFFER_TOO_SMALL−138出力バッファが足りないサイズマクロを使わずに決め打ちした
PSA_ERROR_INVALID_HANDLE−136鍵 ID が無効破棄済みの鍵、未初期化の psa_key_id_t(値 0)
PSA_ERROR_INVALID_SIGNATURE−149署名・MAC・AEAD タグの検証に失敗改ざんされた、または鍵・データ・アルゴリズムのどれかが一致しない
PSA_ERROR_ALREADY_EXISTS−139同じ ID の永続鍵がすでにある前回の実行で作った鍵が残っている(12 章)
PSA_ERROR_DOES_NOT_EXIST−140指定した ID のデータがないストレージの UID(保存データの識別番号。13 章)間違い。鍵が未作成
PSA_ERROR_INSUFFICIENT_MEMORY−141メモリ不足ヒープが小さい。鍵スロット(実装が鍵を保持する枠)数の上限(MBEDTLS_PSA_KEY_SLOT_COUNT)
PSA_ERROR_INSUFFICIENT_STORAGE−142不揮発ストレージの空きがないITS/PS の領域が満杯
PSA_ERROR_INSUFFICIENT_ENTROPY−148乱数の種が足りないエントロピー源(乱数の元になる物理的なゆらぎの供給源)が未設定
PSA_ERROR_STORAGE_FAILURE−146ストレージの読み書きが失敗フラッシュドライバの不具合
PSA_ERROR_HARDWARE_FAILURE−147暗号エンジンなどのハードウェア異常ドライバの実装バグ、クロック未供給
PSA_ERROR_CORRUPTION_DETECTED−151内部データの整合性が壊れているメモリ破壊。攻撃の可能性も
PSA_ERROR_COMMUNICATION_FAILURE−145セキュア側との通信失敗TF-M の呼び出しが途切れた。セキュアエレメントの応答なし
PSA_ERROR_GENERIC_ERROR−132分類できない失敗実装の内部エラー
PSA_ERROR_PROGRAMMER_ERROR−129呼び出し側の明らかな誤用(TF-M で発生)セキュア側に渡したポインタが非セキュア側のメモリを指していない
psa_status_t — 0 が成功、負が失敗。分類で覚えるPSA_SUCCESS = 0成功呼び方の誤り(コードを直す)BAD_STATE −137順序違反・init 忘れINVALID_ARGUMENT −135引数の組み合わせ・形式NOT_PERMITTED −133鍵の用途・方針BUFFER_TOO_SMALL −138サイズマクロを使え環境・設定(ビルドを直す)NOT_SUPPORTED −134PSA_WANT_* の宣言漏れINSUFFICIENT_ENTROPY −148乱数源が未接続INSUFFICIENT_MEMORY −141鍵スロット・ヒープINSUFFICIENT_STORAGE −142ITS/PS が満杯データ・実行時(設計で対処)INVALID_SIGNATURE −149改ざん・鍵違いDOES_NOT_EXIST −140ID 違い・未作成ALREADY_EXISTS −139永続鍵の残りHARDWARE/STORAGE_FAILURE−147 / −146。ドライバ値はすべて psa/error.h で定義され、Crypto でもストレージでも共通
エラーは「コードを直す」「ビルドを直す」「設計で対処する」の 3 群に分けると原因の探し方が決まる

エラーを「握りつぶさない」ための最小の型.

組み込みでは戻り値を無視するコードが横行するが、PSA では無視すると 「暗号化したつもりで平文を送る」という最悪の事態になる。 次の形を全関数に貼る習慣をつけよ。

#define PSA_CHECK(expr) do { psa_status_t _s = (expr); \
    if (_s != PSA_SUCCESS) { log_error(#expr, _s); goto cleanup; } } while (0)

cleanup: ラベルの先で操作オブジェクトを abort し、鍵を破棄する(この章の 5 節)。

エラーコードを読み解く

2. psa_crypto_init() — 全ての前に 1 回

Crypto API は、最初に psa_crypto_init() を呼ぶまで使えない。 これは乱数生成器(RNG)の初期化、鍵ストレージの読み込み、ハードウェアドライバの起動を行う。 呼び忘れると、ほとんどの関数が PSA_ERROR_BAD_STATE を返す。

psa_status_t psa_crypto_init(void);

ストレージ API(13 章)とアテステーション API(14 章)には初期化関数がない。 実装の起動時に準備されている前提である。

psa_crypto_init() が行うことpsa_crypto_init()全 API の前に 1 回乱数生成器の種付けエントロピー源 → DRBG鍵ストレージの準備永続鍵の一覧を読むドライバの起動暗号エンジン・SE の初期化PSA_SUCCESS2 回目以降は何もしない失敗の典型: PSA_ERROR_INSUFFICIENT_ENTROPYマイコンで乱数源(TRNG)を接続していない(15 章)呼び忘れの症状: PSA_ERROR_BAD_STATEほとんどの関数が最初の呼び出しでこれを返す
初期化は乱数・ストレージ・ドライバの準備。何度呼んでも安全なので、各モジュールの先頭で呼んでよい

3. 定数の作り方 — 「種類」を整数で表す

PSA API の引数には、次の 4 種類の整数で表される「種類」が繰り返し登場する。 これらは単なる列挙値ではなく、ビットの区画に意味がある 32 ビット(または 16 ビット)の符号化された値である。 仕組みを知っておくと、エラーメッセージに出た数字の意味を読み解ける。

型例何を表すか
psa_algorithm_t(32 ビット)PSA_ALG_SHA_256 = 0x02000009、PSA_ALG_GCM = 0x05500200アルゴリズム。上位 8 ビットが「分類」(0x02 ハッシュ、0x03 MAC、0x04 暗号、0x05 AEAD、0x06 署名、0x07 公開鍵暗号、0x08 鍵導出、0x09 鍵合意)
psa_key_type_t(16 ビット)PSA_KEY_TYPE_AES = 0x2400、PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1) = 0x7112鍵の種類。上位ビットが「対称 / 公開鍵 / 鍵ペア」、下位に曲線などの詳細
psa_key_usage_t(32 ビット)PSA_KEY_USAGE_SIGN_HASH = 0x1000用途。1 ビット 1 用途で、OR で組み合わせる
psa_key_lifetime_t(32 ビット)PSA_KEY_LIFETIME_PERSISTENT = 1揮発 / 永続と、鍵の置き場所

アルゴリズムはマクロで組み立てることが多い。 PSA_ALG_HMAC(PSA_ALG_SHA_256) は「SHA-256 を使う HMAC」であり、 PSA_ALG_ECDSA(PSA_ALG_SHA_256) は「SHA-256 でハッシュしてから ECDSA 署名」である。 括弧の中にハッシュを入れるという規則を覚えれば、ほとんどのアルゴリズム定数が読める。

PSA_ALG_HMAC(PSA_ALG_SHA_256)            /* MAC       0x03800009 */
PSA_ALG_ECDSA(PSA_ALG_SHA_256)           /* 署名      0x06000609 */
PSA_ALG_RSA_OAEP(PSA_ALG_SHA_256)        /* 公開鍵暗号 0x07000309 */
PSA_ALG_HKDF(PSA_ALG_SHA_256)            /* 鍵導出    0x08000109 */
PSA_ALG_KEY_AGREEMENT(PSA_ALG_ECDH, PSA_ALG_HKDF(PSA_ALG_SHA_256))   /* 鍵合意+導出 */

逆向きに調べるマクロもある。 PSA_ALG_IS_HASH(alg)、PSA_ALG_IS_MAC(alg)、PSA_ALG_GET_HASH(alg)(署名や MAC から中のハッシュを取り出す)、 PSA_KEY_TYPE_IS_ASYMMETRIC(type)、PSA_KEY_TYPE_ECC_GET_FAMILY(type) などである。 汎用的なコード(複数のアルゴリズムを引数で受ける関数)を書くときに使う。

psa_algorithm_t(32 ビット)のビット区画分類 8bit0x02 ハッシュ 0x03 MAC 0x04 暗号方式・詳細 16bitアルゴリズムの種類、パラメータハッシュ 8bit内包するハッシュの ID(0x09 = SHA-256)分類: 0x05 AEAD 0x06 署名 0x07 公開鍵暗号 0x08 鍵導出 0x09 鍵合意 0x0a PAKEPSA_ALG_SHA_256 = 0x02000009 ← 分類 02、ハッシュ 09PSA_ALG_HMAC(PSA_ALG_SHA_256) = 0x03800009 ← 分類 03(MAC)+ HMAC + SHA-256PSA_ALG_GCM = 0x05500200 ← 分類 05(AEAD)、GCMPSA_ALG_ECDSA(PSA_ALG_SHA_256) = 0x06000609 ← 分類 06(署名)+ ECDSA + SHA-256PSA_ALG_HKDF(PSA_ALG_SHA_256) = 0x08000109 ← 分類 08(鍵導出)+ HKDF + SHA-256PSA_ALG_KEY_AGREEMENT(PSA_ALG_ECDH, PSA_ALG_HKDF(PSA_ALG_SHA_256)) = 0x09020109 ← 分類 09(鍵合意)+ ECDH + HKDF + SHA-256括弧の中にハッシュを入れて組み立てる。PSA_ALG_GET_HASH(alg) で取り出せる
アルゴリズム定数は「分類 + 方式 + ハッシュ」の符号化された整数。上位 8 ビットを見れば何の操作か分かる

4. 出力バッファの流儀 — 「サイズ」と「長さ」のペア

PSA 関数が結果をバイト列として返すときは、必ず次の 3 つ組で受け取る。

uint8_t buf[SIZE];      /* 出力先 */
size_t  buf_size = sizeof buf;  /* バッファの大きさ(呼ぶ側が伝える) */
size_t  out_len;        /* 実際に書かれた長さ(関数が返す) */

st = psa_xxx(..., buf, buf_size, &out_len);

バッファの大きさは、サイズマクロで決める。決め打ちの数字(「AES だから 16」)は、アルゴリズムを変えた瞬間に壊れる。

マクロ何のサイズか
PSA_HASH_LENGTH(alg)ハッシュの出力長(SHA-256 なら 32)
PSA_MAC_LENGTH(key_type, key_bits, alg)MAC の出力長
PSA_CIPHER_ENCRYPT_OUTPUT_SIZE(key_type, alg, input_length)暗号文の長さ(IV 込み)
PSA_AEAD_ENCRYPT_OUTPUT_SIZE(key_type, alg, plaintext_length)AEAD 暗号文(タグ込み)
PSA_SIGN_OUTPUT_SIZE(key_type, key_bits, alg)署名の長さ
PSA_EXPORT_KEY_OUTPUT_SIZE(key_type, key_bits)鍵をエクスポートしたときの長さ
PSA_EXPORT_PUBLIC_KEY_OUTPUT_SIZE(key_type, key_bits)公開鍵をエクスポートしたときの長さ
PSA_RAW_KEY_AGREEMENT_OUTPUT_SIZE(key_type, key_bits)鍵合意の共有秘密の長さ
PSA_HASH_MAX_SIZE、PSA_MAC_MAX_SIZE、PSA_SIGNATURE_MAX_SIZE など「この実装で扱う全アルゴリズムの最大」。アルゴリズムが実行時に決まるときに使う

PSA_BITS_TO_BYTES(bits)(ビットをバイトに切り上げ)と PSA_BYTES_TO_BITS(bytes) も頻出である。 鍵長はビットで指定し(psa_set_key_bits(&attr, 256))、データはバイトで扱うので、変換が必要になる。

出力バッファの 3 つ組 — (buf, buf_size, &out_len)実際に書かれた out_len バイト未使用buf_size(呼ぶ側が伝える上限)out_len(関数が返す実長)足りないときPSA_ERROR_BUFFER_TOO_SMALL を返し、何も書かない(部分出力なし)。out_len も不定buf_size はサイズマクロで決める:uint8_t sig[PSA_SIGN_OUTPUT_SIZE(PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1), 256, PSA_ALG_ECDSA(PSA_ALG_SHA_256))]; /* = 64 */uint8_t ct[PSA_AEAD_ENCRYPT_OUTPUT_SIZE(PSA_KEY_TYPE_AES, PSA_ALG_GCM, len)]; /* = len+16 */uint8_t h[PSA_HASH_MAX_SIZE]; /* アルゴリズムが実行時に決まるなら MAX を使う */
「サイズ」は上限、「長さ」は実長。決め打ちの数字はアルゴリズムを変えた瞬間に壊れるので、マクロで計算する

なぜ「サイズと長さの 2 つ」なのか.

「関数が必要な分だけ malloc して返す」設計にしなかったのは、 PSA が動的メモリのないマイコンでも動くことを最優先したからである。 呼ぶ側が静的配列を用意し、関数はそこに書く。 そしてセキュア側(TF-M)から非セキュア側のメモリに書き込むときは、 「どこからどこまで書いてよいか」を明示する必要があり、そのために buf_size が要る。

5. 操作オブジェクト — 分割処理の作法

ハッシュ・MAC・暗号・AEAD・鍵導出には、「一発で処理する関数」のほかに、 データを分割して少しずつ処理する関数群がある。 1 MB のファームウェアイメージをハッシュするとき、全体をメモリに置けないからだ。

分割処理は、操作オブジェクト(operation object)という構造体を使い、 決まった順番で関数を呼ぶ。

psa_hash_operation_t op = PSA_HASH_OPERATION_INIT;   /* ① 初期化子で宣言 */
PSA_CHECK(psa_hash_setup(&op, PSA_ALG_SHA_256));     /* ② setup:アルゴリズム決定 */
PSA_CHECK(psa_hash_update(&op, chunk1, len1));       /* ③ update:何回でも */
PSA_CHECK(psa_hash_update(&op, chunk2, len2));
PSA_CHECK(psa_hash_finish(&op, hash, sizeof hash, &hash_len));  /* ④ finish:結果を取り出す */

すべての操作オブジェクトに共通する規則:

  1. 必ず初期化子で宣言する。PSA_HASH_OPERATION_INIT、PSA_MAC_OPERATION_INIT、PSA_CIPHER_OPERATION_INIT、PSA_AEAD_OPERATION_INIT、PSA_KEY_DERIVATION_OPERATION_INIT。または psa_xxx_operation_init() 関数で初期化する。ゼロ初期化されていないオブジェクトに setup すると動作は未定義
  2. 状態は「inactive → active → inactive」。setup で active になり、finish / verify / abort で inactive に戻る。active でないときに update すると PSA_ERROR_BAD_STATE
  3. どこかでエラーが出たら、必ず abort する。エラーが出た時点でオブジェクトは「壊れた状態」であり、abort で inactive に戻さないと再利用できない。abort は inactive なオブジェクトに対して呼んでも安全(何もしない)
  4. finish が成功したら abort は不要だが、呼んでも害はない。「エラー経路で必ず abort」を守るために、cleanup: で無条件に abort するのが一番簡単
  5. オブジェクトをコピーしてはいけない(memcpy や代入)。中に実装依存の状態やハードウェアのハンドル(暗号エンジン側の処理を指す参照番号)が入っている。ハッシュだけは psa_hash_clone() という専用の複製関数がある
操作オブジェクトの状態遷移(ハッシュ・MAC・暗号・AEAD・鍵導出に共通)inactiveINIT 直後 / finish 後 / abort 後activeupdate を受け付けるerrorabort 以外は BAD_STATEsetup 成功finish / verify 成功どれかが失敗abort(どの状態からでも inactive へ。inactive に対しても安全)update(何回でも)エラーが出たら必ず abort する。cleanup で無条件に abort するのが最も簡単
setup で active、finish で inactive に戻る。失敗すると error になり、abort でしか戻れない

6. 鍵 ID の作法

鍵を表す型は psa_key_id_t(32 ビット符号なし整数)である。 値 0 は PSA_KEY_ID_NULL(「鍵なし」)で、未初期化の変数を渡すと PSA_ERROR_INVALID_HANDLE になる。

psa_key_id_t key = PSA_KEY_ID_NULL;   /* 宣言時に NULL にしておく癖 */
...
if (key != PSA_KEY_ID_NULL) psa_destroy_key(key);   /* cleanup で安全に破棄 */

古い資料には psa_key_handle_t と psa_open_key() / psa_close_key() が出てくる。 これは Crypto API 1.0 で廃止された古い作法で、現在は鍵 ID をそのまま渡す。 Mbed TLS 3.x で mbedtls_svc_key_id_t という型を見ることがあるが、 これは TF-M 環境で「鍵の所有者(どのクライアントが作ったか)」を鍵 ID と一緒に持つための型で、通常は psa_key_id_t と同じものだと思ってよい。

7. 全体の型 — 1 つの関数を呼ぶまでのテンプレート

ここまでの作法を 1 つにまとめると、PSA を使う関数は次の骨格になる。 以後の章のコードは、すべてこの骨格の変奏である。

psa_status_t do_something(const uint8_t *in, size_t in_len,
                          uint8_t *out, size_t out_size, size_t *out_len)
{
    psa_status_t st;
    psa_key_id_t key = PSA_KEY_ID_NULL;
    psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
    psa_xxx_operation_t op = PSA_XXX_OPERATION_INIT;

    st = psa_crypto_init();
    if (st != PSA_SUCCESS) goto cleanup;

    /* 鍵の属性を宣言して鍵を用意する([03 章](03_鍵と属性.md)・[04 章](04_鍵の作成と破棄.md)) */
    psa_set_key_type(&attr, ...);
    psa_set_key_usage_flags(&attr, ...);
    psa_set_key_algorithm(&attr, ...);
    st = psa_import_key(&attr, ..., &key);
    if (st != PSA_SUCCESS) goto cleanup;

    /* 操作する */
    st = psa_xxx_setup(&op, key, alg);          if (st != PSA_SUCCESS) goto cleanup;
    st = psa_xxx_update(&op, in, in_len);        if (st != PSA_SUCCESS) goto cleanup;
    st = psa_xxx_finish(&op, out, out_size, out_len);

cleanup:
    psa_xxx_abort(&op);                 /* 無条件。inactive なら何もしない */
    psa_reset_key_attributes(&attr);    /* 属性構造体の後片付け([04 章](04_鍵の作成と破棄.md)) */
    if (key != PSA_KEY_ID_NULL) psa_destroy_key(key);
    return st;
}

この章のポイント