PSA APIs 05 · 乱数とハッシュ — 鍵を使わない 2 つの基本操作

Chapter 05

乱数とハッシュ — 鍵を使わない 2 つの基本操作

この章のゴール.

psa_generate_random() と psa_hash_*() を、 「一発関数」と「分割関数」の両方で使えるようになり、 なぜ分割関数の状態遷移がこの形なのかを説明できるようになること。

この章で使う既出の用語(定義は各リンク先). PSA(01 章 1 節)、ビット(02 章 4 節)、操作オブジェクト(02 章 5 節)、algorithm(03 章 11 節)、generate(04 章 10 節)、import(04 章 10 節)、作成(04 章 1 節)、使用(04 章 1 節)

1. この 2 つだけは鍵が要らない

Crypto API のほとんどの操作は鍵 ID を受け取るが、乱数生成とハッシュだけは鍵を使わない。 鍵の属性の話をひとまず忘れて、API の作法(02 章)そのものを練習するのに最適である。

2. psa_generate_random() — 乱数

psa_status_t psa_generate_random(uint8_t *output, size_t output_size);

引数はバッファとその長さだけで、output_size バイトが必ず埋まる(この関数だけは「実長」を返さない)。

uint8_t nonce[12];
PSA_CHECK(psa_generate_random(nonce, sizeof nonce));

PSA の乱数は DRBG(Deterministic Random Bit Generator、決定論的乱数生成器)—— 物理的なエントロピー源(乱数の元になる予測不能なゆらぎの供給源)から得た「種」を元に、 暗号学的に安全な擬似乱数を大量に作る仕組み——の出力である。 Mbed TLS では CTR_DRBG(AES を使う DRBG)か HMAC_DRBG が使われる。

初心者が知っておくべきことは 3 つである。

  1. rand() や HAL_GetTick() を鍵やノンスに使ってはいけない。予測できる乱数は、暗号を無力化する(過去に多数の実被害がある)
  2. psa_generate_random() が PSA_ERROR_INSUFFICIENT_ENTROPY を返すなら、エントロピー源が設定されていない。マイコンでは TRNG(True Random Number Generator、物理現象を使う真性乱数ハードウェア)を Mbed TLS に接続する作業が必要で、これが動かないと psa_crypto_init() 自体が失敗する(15 章)
  3. 乱数を「鍵」として使いたいなら、psa_generate_random() で作って psa_import_key() するのではなく、psa_generate_key() を使う。値がアプリケーションのメモリを通らないで済む
psa_generate_random() の中身 — エントロピー源と DRBGエントロピー源TRNG(物理ゆらぎ)や OS の乱数種(seed)数十バイトの真の乱数DRBGCTR_DRBG / HMAC_DRBG。種から大量の乱数を生成psa_generate_randomoutput_size バイトを必ず埋めるマイコンで INSUFFICIENT_ENTROPY が出たらエントロピー源が未接続。mbedtls_hardware_poll() か MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG を実装する(15 章)rand() / HAL_GetTick() は使わない予測できる乱数は暗号を無力化する。鍵にしたいなら psa_generate_key() を使う
物理的な乱数源から取った種を DRBG で伸ばす。種がなければ初期化そのものが失敗する

3. ハッシュ — 一発関数

psa_status_t psa_hash_compute(psa_algorithm_t alg,
                              const uint8_t *input, size_t input_length,
                              uint8_t *hash, size_t hash_size, size_t *hash_length);

psa_status_t psa_hash_compare(psa_algorithm_t alg,
                              const uint8_t *input, size_t input_length,
                              const uint8_t *hash, size_t hash_length);

psa_hash_compute() はデータを渡すとハッシュを返す。 psa_hash_compare() は「このデータのハッシュは、この値と一致するか」を判定し、一致なら PSA_SUCCESS、不一致なら PSA_ERROR_INVALID_SIGNATURE を返す。

uint8_t digest[PSA_HASH_LENGTH(PSA_ALG_SHA_256)];
size_t  digest_len;
PSA_CHECK(psa_hash_compute(PSA_ALG_SHA_256, data, data_len, digest, sizeof digest, &digest_len));

/* ダウンロードしたイメージの期待値と照合 */
st = psa_hash_compare(PSA_ALG_SHA_256, image, image_len, expected, 32);
if (st == PSA_ERROR_INVALID_SIGNATURE) { /* 壊れているか改ざん */ }

なぜ memcmp ではなく psa_hash_compare なのか.

memcmp は最初に違うバイトが見つかった瞬間に終わるので、一致した長さが処理時間に現れる。 攻撃者は応答時間を測ることで、1 バイトずつ正解に近づける(タイミング攻撃)。 ハッシュの照合ではこれは致命的ではないが、MAC の照合(06 章)では致命的であり、 PSA は「比較は必ずライブラリの中で、一定時間で」という作法を全体で統一している。 だから psa_hash_compare、psa_mac_verify、psa_verify_hash という「検証専用関数」が用意されている。

使えるハッシュのアルゴリズム定数:

定数出力(バイト)用途
PSA_ALG_SHA_25632標準。迷ったらこれ
PSA_ALG_SHA_38448P-384 と組む。TLS の一部
PSA_ALG_SHA_51264Ed25519 の内部で使用
PSA_ALG_SHA_22428古い規格の互換
PSA_ALG_SHA3_25632SHA-3 系(SHA-2 とは別設計の新しいハッシュ族)。対応実装は少ない
PSA_ALG_SHA_120新規設計では使わない(衝突が発見済み)。既存プロトコルの互換のみ
PSA_ALG_MD516同上。互換のみ

出力長は PSA_HASH_LENGTH(alg) で得られる。アルゴリズムが実行時に決まるなら PSA_HASH_MAX_SIZE(実装が対応する最大。通常 64)でバッファを取る。

ハッシュ — 任意長から固定長の「指紋」hello5 バイトhellp(1 文字違い)5 バイト1 MB のファームウェアSHA-256psa_hash_compute2cf24dba…9824 (32 バイト)同じ入力なら必ず同じ6b28c6e8…f1a0 (32 バイト)1 ビット違えば全く別a5e1…ff3c (32 バイト)出力から入力は逆算できない出力長は PSA_HASH_LENGTH(alg)。ハッシュに鍵はなく、誰でも同じ値を計算できる
どんな長さでも 32 バイトになる。似た入力でも出力は無関係に見える

4. ハッシュ — 分割関数

psa_hash_operation_t op = PSA_HASH_OPERATION_INIT;
psa_status_t psa_hash_setup(psa_hash_operation_t *op, psa_algorithm_t alg);
psa_status_t psa_hash_update(psa_hash_operation_t *op, const uint8_t *input, size_t input_length);
psa_status_t psa_hash_finish(psa_hash_operation_t *op, uint8_t *hash, size_t hash_size, size_t *hash_length);
psa_status_t psa_hash_verify(psa_hash_operation_t *op, const uint8_t *hash, size_t hash_length);
psa_status_t psa_hash_abort(psa_hash_operation_t *op);
psa_status_t psa_hash_clone(const psa_hash_operation_t *src, psa_hash_operation_t *dst);

ファームウェアイメージのように全体をメモリに置けないデータや、 通信のように少しずつ届くデータをハッシュするときは、分割関数を使う。

psa_status_t hash_flash_region(uint32_t addr, size_t total, uint8_t out[32])
{
    psa_hash_operation_t op = PSA_HASH_OPERATION_INIT;
    uint8_t buf[256];
    size_t out_len, done = 0;
    psa_status_t st;

    st = psa_hash_setup(&op, PSA_ALG_SHA_256);
    if (st != PSA_SUCCESS) goto cleanup;
    while (done < total) {
        size_t n = (total - done < sizeof buf) ? total - done : sizeof buf;
        flash_read(addr + done, buf, n);
        st = psa_hash_update(&op, buf, n);          /* 何回でも。長さは任意 */
        if (st != PSA_SUCCESS) goto cleanup;
        done += n;
    }
    st = psa_hash_finish(&op, out, 32, &out_len);   /* 成功すると op は inactive に戻る */
cleanup:
    psa_hash_abort(&op);                            /* 失敗時の後始末。成功時は無害 */
    return st;
}

4.1 状態遷移

分割ハッシュの操作オブジェクトは、次の状態を持つ。

update に渡すデータはどう分割しても結果は同じである。 「256 バイトずつ」でも「1 バイトずつ」でも「全部一度に」でも、finish の出力は一致する。 これは、ハッシュ関数が内部で 64 バイト(SHA-256 の場合)のブロック単位で処理しつつ、端数を操作オブジェクトの中に溜めているからである。

分割ハッシュ — どう分けても結果は同じ全体を一度にpsa_hash_compute(全体 1024 B)256 バイトずつ updateupdate 256update 256update 256update 256不揃いに update105001513finish → 同じ 32 バイト内部は 64 バイトのブロック単位で処理し、端数を操作オブジェクトに溜めているsetup → update(任意回・任意長)→ finish。エラー時は abort。psa_hash_clone で途中状態を分岐できる
分割の仕方は結果に影響しない。全体をメモリに置けないデータでも同じハッシュが得られる

4.2 psa_hash_clone() — 途中まで計算した状態を分岐する

ハッシュだけにある特別な関数が psa_hash_clone() である。 active な操作オブジェクトを複製し、「ここまでの共通部分 + それぞれ違う末尾」を 2 回計算する手間を省く。

典型は、共通ヘッダの後に複数のバリアント(末尾だけ違う版)が続く場合や、 TLS のハンドシェイク(通信の冒頭で鍵を決める手順)で「ここまでの全メッセージのハッシュ」を何度も取る場合である。

psa_hash_operation_t base = PSA_HASH_OPERATION_INIT, branch = PSA_HASH_OPERATION_INIT;
psa_hash_setup(&base, PSA_ALG_SHA_256);
psa_hash_update(&base, header, header_len);        /* 共通部分 */

psa_hash_clone(&base, &branch);                    /* branch は inactive でなければならない */
psa_hash_update(&branch, tail_a, tail_a_len);
psa_hash_finish(&branch, hash_a, 32, &len);        /* ヘッダ+A のハッシュ */

psa_hash_update(&base, tail_b, tail_b_len);
psa_hash_finish(&base, hash_b, 32, &len);          /* ヘッダ+B のハッシュ */

memcpy で操作オブジェクトを複製してはいけない理由もここにある—— ハードウェアアクセラレータ(暗号計算を肩代わりする専用回路)を使う実装では、操作の状態がハードウェアの中にあり、構造体のコピーでは複製にならない。 psa_hash_clone() は、実装がそれを正しく行う唯一の方法である。

5. ハッシュが使われる場面と、PSA での担当

「ハッシュを取る」という操作は単独では意味を持たず、常に何かの部品である。 PSA の中でどの操作がハッシュを内包しているかを知っておくと、自分でハッシュを取る必要があるかどうかが判断できる。

場面自分でハッシュを取る?PSA での呼び方
ファイル・イメージの完全性確認するpsa_hash_compute / psa_hash_compare
署名の作成・検証どちらでも。psa_sign_message は内部で取る。psa_sign_hash は自分で取って渡す09 章
MACしない(HMAC の内部でハッシュが回る)06 章
鍵導出しない(HKDF の内部で HMAC が回る)10 章
パスワードの保存ハッシュ単体は不可。PBKDF2 を使う10 章
TLS ハンドシェイクの転写ハッシュ分割ハッシュ + cloneライブラリの内部

ハッシュは「秘密」を守らない.

ハッシュには鍵がないので、誰でも同じ値を計算できる。 「ハッシュを付けたから改ざんが分かる」は、攻撃者がデータとハッシュの両方を書き換えられるなら成り立たない。 改ざん検出に必要なのは鍵付きのハッシュ、すなわち次章の MAC か、09 章の署名である。 ハッシュ単体で意味があるのは、ハッシュ値そのものが信頼できる経路で届く場合(署名済みのマニフェスト——更新内容の一覧を記した目録——に書かれている、など)だけである。

6. 手を動かす

ハッシュの性質を確かめる

7. 仕様書に逃がす

関数・定数用途
psa_hash_suspend() / psa_hash_resume()途中状態をバイト列にして保存・復元する(Crypto API 1.1 で追加。長時間の処理を中断するとき)。対応実装は少ない
PSA_HASH_BLOCK_LENGTH(alg)ハッシュ関数の内部ブロック長(HMAC の鍵長の目安を出すとき)
PSA_ALG_SHAKE256_512、PSA_ALG_SHA3_*SHA-3 系。ポスト量子暗号(量子コンピュータでも破れないよう設計された暗号)の実装で使われ始めている

この章のポイント