PSA APIs 09 · 署名と検証 — 公開鍵で「誰が」を証明する

Chapter 09

署名と検証 — 公開鍵で「誰が」を証明する

この章のゴール.

psa_sign_hash / psa_verify_hash と psa_sign_message / psa_verify_message の違い、 ECDSA・EdDSA・RSA の署名のバイト列形式、 そして「ハッシュ長は署名アルゴリズムのハッシュと一致させる」規則を理解し、 装置の認証や OTA イメージの検証を書けるようになること。

この章で使う既出の用語(定義は各リンク先). PSA(01 章 1 節)、ビット(02 章 4 節)、algorithm(03 章 11 節)、type(03 章 11 節)、usage(03 章 11 節)、場所(03 章 7 節)、import(04 章 10 節)、ビッグエンディアン(04 章 2 節)、ハッシュ(05 章 1 節)、乱数(05 章 1 節)、MAC(06 章 1 節)、対称暗号(07 章 1 節)

1. 署名とは何か — MAC との違い

デジタル署名は、秘密鍵で作り、公開鍵で検証する認証値である。 MAC(06 章)と違い、検証できる者が作れるとは限らない。 検証側は公開鍵しか持たないので、偽造できない。 だから署名は「誰が作ったか」の証明——認証(authentication)と、後で「作っていない」と言えなくする否認防止(non-repudiation)に使える。

MAC署名
鍵1 つの共有鍵秘密鍵(作る側)と公開鍵(検証側)
検証者は偽造できるかできる(同じ鍵を持つ)できない
速度速い(µs)遅い(ms〜数十 ms)
出力長16〜32 バイト64(ECDSA P-256)〜256(RSA-2048)バイト
典型的用途通信フレームの認証ファームウェアの検証、装置の認証、証明書
MAC と署名 — 検証者が偽造できるかMAC(対称)送信者: 鍵 Kタグを作る受信者: 同じ鍵 Kタグを検証する受信者も正しいタグを作れる → 「送信者が作った」とは第三者に証明できない署名(非対称)署名者: 秘密鍵 d署名を作る検証者: 公開鍵 Q検証だけ検証者は秘密鍵を持たないので偽造できない → 認証と否認防止速度: MAC は µs、署名は ms〜数十 ms。出力: MAC 16〜32 B、署名 64〜256 B通信フレームは MAC、ファームウェア検証と装置認証は署名
検証者が生成もできる MAC に対し、署名は公開鍵しか持たない者にも検証できる

2. アルゴリズムと鍵種別

アルゴリズム定数鍵種別署名長特徴
PSA_ALG_ECDSA(hash)ECC_KEY_PAIR(SECP_R1) 256 ビット64(P-256)標準。署名時に乱数が必要(乱数が悪いと秘密鍵が漏れる)
PSA_ALG_DETERMINISTIC_ECDSA(hash)同上64乱数を使わない ECDSA(RFC 6979)。検証は通常の ECDSA と同じ。乱数源に不安があるマイコンではこちら
PSA_ALG_PURE_EDDSAECC_KEY_PAIR(TWISTED_EDWARDS) 255 ビット64Ed25519。決定論的で高速、実装の落とし穴が少ない。sign_message 専用(ハッシュ分離不可)
PSA_ALG_ED25519PH同上64メッセージを先にハッシュする Ed25519 変種
PSA_ALG_RSA_PSS(hash)RSA_KEY_PAIR 2048 以上256(2048 ビット)RSA の現代的な署名。乱数のソルト(計算に混ぜる使い捨ての値)を使う
PSA_ALG_RSA_PKCS1V15_SIGN(hash)同上256古い RSA 署名。互換のため広く残っている
PSA_ALG_ECDSA_ANYECC64ハッシュを指定しない。既にハッシュ済みのデータを sign_hash するときの互換用

新規設計では ECDSA P-256 + SHA-256、または Ed25519である。 RSA は署名が 4 倍長く、鍵生成が遅く、マイコンでの検証も遅いが、「サーバ側が RSA しか出せない」ことがまだある。

鍵の属性は、署名側が PSA_KEY_USAGE_SIGN_HASH(または SIGN_MESSAGE)、検証側が PSA_KEY_USAGE_VERIFY_HASH(または VERIFY_MESSAGE)。 03 章の表のとおり、SIGN_HASH は SIGN_MESSAGE を含む(ハッシュに署名できるなら、メッセージをハッシュして署名することもできる)。逆は含まない。

3. hash 版と message 版

署名の関数は 2 組ある。

/* ハッシュ済みのデータに署名する */
psa_status_t psa_sign_hash(psa_key_id_t key, psa_algorithm_t alg,
                           const uint8_t *hash, size_t hash_length,
                           uint8_t *signature, size_t signature_size, size_t *signature_length);
psa_status_t psa_verify_hash(psa_key_id_t key, psa_algorithm_t alg,
                             const uint8_t *hash, size_t hash_length,
                             const uint8_t *signature, size_t signature_length);

/* メッセージそのものを渡す(内部でハッシュする) */
psa_status_t psa_sign_message(psa_key_id_t key, psa_algorithm_t alg,
                              const uint8_t *input, size_t input_length,
                              uint8_t *signature, size_t signature_size, size_t *signature_length);
psa_status_t psa_verify_message(psa_key_id_t key, psa_algorithm_t alg,
                                const uint8_t *input, size_t input_length,
                                const uint8_t *signature, size_t signature_length);
sign_hash / verify_hashsign_message / verify_message
渡すもの自分で計算したハッシュ(SHA-256 なら 32 バイト)元データ
ハッシュの計算呼ぶ側(05 章の分割ハッシュで、大きなイメージでも可)ライブラリの内部
Ed25519使えない(PURE_EDDSA はメッセージ全体が必要)使える
使いどころ大きなデータ、TLS のように「ハッシュだけ」が手元にある場合小さなデータ(チャレンジ応答など)

sign_hash に渡すハッシュの長さは、アルゴリズム定数の中のハッシュの出力長と一致していなければならない。 PSA_ALG_ECDSA(PSA_ALG_SHA_256) に 20 バイトの SHA-1 ハッシュを渡すと PSA_ERROR_INVALID_ARGUMENT である。 ECDSA は数学的にはどんな長さでも受け付けるが、PSA は「宣言と違う使い方」を禁止する。

hash 版と message 版psa_sign_hash / psa_verify_hashアプリが psa_hash_* でハッシュを計算大きなデータを分割で。TLS のように「ハッシュだけ」がある場合psa_sign_hash(key, ECDSA(SHA_256), hash, 32, …)ハッシュ長はアルゴリズムのハッシュと一致psa_sign_message / psa_verify_message元データをそのまま渡す小さなデータ。チャレンジ応答など内部でハッシュしてから署名Ed25519(PURE_EDDSA)はこちらのみ用途: SIGN_HASH は SIGN_MESSAGE を含む。逆は含まない
ハッシュを自分で取るか、ライブラリに任せるか。Ed25519 はメッセージ全体が必要なので message 版だけ

4. ファームウェアイメージの検証 — 典型コード

OTA(無線ファームウェア更新)で受け取ったイメージの署名を検証する、最も典型的な使い方である。 イメージは大きいので、分割ハッシュ + verify_hash で書く。

/* 製造時に書き込んだ、更新サーバの公開鍵(65 バイト 04||X||Y) */
static const uint8_t server_pub[65] = { 0x04, ... };

psa_status_t verify_image(uint32_t addr, size_t len, const uint8_t sig[64])
{
    psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
    psa_key_id_t pub = PSA_KEY_ID_NULL;
    uint8_t hash[32]; size_t hash_len;
    psa_status_t st;

    /* 公開鍵を揮発鍵として取り込む(毎回でよい。安い) */
    psa_set_key_type(&attr, PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1));
    psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_VERIFY_HASH);
    psa_set_key_algorithm(&attr, PSA_ALG_ECDSA(PSA_ALG_SHA_256));
    st = psa_import_key(&attr, server_pub, sizeof server_pub, &pub);
    psa_reset_key_attributes(&attr);
    if (st != PSA_SUCCESS) goto cleanup;

    st = hash_flash_region(addr, len, hash);          /* [05 章](05_乱数とハッシュ.md#psahashclone-途中まで計算した状態を分岐する)の関数 */
    if (st != PSA_SUCCESS) goto cleanup;

    st = psa_verify_hash(pub, PSA_ALG_ECDSA(PSA_ALG_SHA_256), hash, 32, sig, 64);
    /* PSA_SUCCESS: 正当  /  PSA_ERROR_INVALID_SIGNATURE: 不正 */
cleanup:
    psa_destroy_key(pub);
    return st;
}

公開鍵を「揮発鍵として毎回 import する」でよいのか.

公開鍵は秘密ではなく、import は数十 µs で終わる。 永続鍵にする理由は「ID で参照したい」「TF-M のセキュア側に置いて非セキュア側から差し替えられないようにしたい」場合である。 後者は重要である——検証用の公開鍵を攻撃者が差し替えられるなら、署名検証は無意味になる。 ブートローダの公開鍵は書き換え不能な領域(OTP や TF-M の read-only 鍵)に置く(iotsec 編 05 章)。

5. 装置の認証 — チャレンジ応答

サーバが「お前は本当に装置 #1234 か」を確かめる手順である。

  1. サーバが乱数(チャレンジ、32 バイト)を送る
  2. 装置が秘密鍵でチャレンジに署名して返す
  3. サーバが登録済みの公開鍵で検証する

装置側:

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 */
size_t sig_len;
PSA_CHECK(psa_sign_message(KEY_ID_DEVICE_SIGN, PSA_ALG_ECDSA(PSA_ALG_SHA_256),
                           challenge, 32, sig, sizeof sig, &sig_len));

チャレンジが乱数である理由は、再送攻撃(過去の署名を再利用する)を防ぐためである。 また、サーバが送ってきた任意のデータにそのまま署名してはいけない——署名対象に「これはチャレンジ応答である」という種別バイトや文脈情報を混ぜる(14 章のアテステーションはこの問題を構造的に解決する)。

チャレンジ応答による装置の認証サーバ装置チャレンジ(32 バイトの乱数)psa_sign_message(DEVICE_KEY, ECDSA(SHA_256), challenge)署名(64 バイト)登録済み公開鍵で psa_verify_hash → SUCCESS なら本物チャレンジが乱数なので、過去の署名を再利用できない(再送攻撃の防止)
サーバの乱数に装置が署名し、登録済みの公開鍵で検証する。秘密鍵は装置から出ない

6. 署名のバイト列形式

署名の形式は、他のライブラリと相互運用するときに必ず躓く点である。

アルゴリズムPSA の形式他で見る形式
ECDSAr ∥ s の生の連結。各 ceil(bits/8) バイト、ビッグエンディアン、0 詰め(P-256 で 32+32 = 64 バイト)OpenSSL、X.509、JWT の一部は DER(30 44 02 20 r 02 20 s、70〜72 バイト)。変換が必要
Ed25519R ∥ S、64 バイト(RFC 8032)同じ
RSA鍵長と同じバイト数の整数、ビッグエンディアン(2048 ビットで 256 バイト)同じ

DER 形式の ECDSA 署名を PSA に渡すには、30 len 02 len r 02 len s を解析して r と s を取り出し、それぞれ 32 バイトに 0 詰めして連結する。 r や s の先頭バイトが 0x80 以上のとき DER は 0x00 を前置して 33 バイトにするので、その 0x00 を落とす必要がある。 Mbed TLS では mbedtls_ecdsa_der_to_raw() / mbedtls_ecdsa_raw_to_der() が用意されている(3.6 以降)。

ECDSA 署名の形式 — PSA は r ∥ s の生連結PSA(64 バイト固定)r(32、0 詰め)s(32、0 詰め)DER(OpenSSL、X.509、70〜72 バイト可変)30 len02 len(00) r02 len(00) s相互変換r や s の先頭が 0x80 以上なら DER は 0x00 を前置する。mbedtls_ecdsa_der_to_raw / raw_to_der(3.6〜)DER をそのまま psa_verify_hash に渡すと INVALID_SIGNATURE または INVALID_ARGUMENT
他のライブラリと署名をやり取りするときは、まず形式を疑う

7. 公開鍵暗号(非対称暗号化)

署名と同じ公開鍵の仕組みを「暗号化」に使うのが非対称暗号化である。 公開鍵で暗号化し、秘密鍵でしか復号できない。用途は小さな秘密(対称鍵など)を相手に渡すことに限られる(RSA-2048 で最大 190 バイト程度。速度も遅い)。

psa_status_t psa_asymmetric_encrypt(psa_key_id_t key, psa_algorithm_t alg,
                                    const uint8_t *input, size_t input_length,
                                    const uint8_t *salt, size_t salt_length,
                                    uint8_t *output, size_t output_size, size_t *output_length);
psa_status_t psa_asymmetric_decrypt(psa_key_id_t key, psa_algorithm_t alg, ...同じ引数...);
アルゴリズム定数特徴
PSA_ALG_RSA_OAEP(hash)現代的な詰め物方式。こちらを使う。salt は OAEP のラベル(通常 NULL, 0)
PSA_ALG_RSA_PKCS1V15_CRYPT古い方式。パディングオラクル攻撃の温床。互換のみ

楕円曲線には「暗号化」の関数がない。楕円曲線で秘密を共有するには鍵合意(11 章)を使う。 実際の設計では、RSA-OAEP よりも ECDH + AEAD(11 章の「ハイブリッド」)を選ぶことが多い。

8. 手を動かす

署名の流れとサイズを見る

9. 仕様書に逃がす

関数・定数用途
psa_sign_hash_start() / psa_sign_hash_complete() / psa_verify_hash_start() / psa_verify_hash_complete()中断可能な署名(Crypto API 1.1)。数十 ms かかる署名を小刻みに進めてリアルタイム性(決められた時間内に応答する性質)を保つ。psa_interruptible_set_max_ops() で 1 回あたりの計算量を制限
PSA_ALG_RSA_PKCS1V15_SIGN_RAWハッシュを自前で DigestInfo(ハッシュの種類と値を DER で包んだ構造)化して渡す RSA 署名
PSA_ALG_ED448PH、PSA_ALG_ECDSA_ANY特殊用途
PSA_SIGNATURE_MAX_SIZEアルゴリズムが実行時に決まるときのバッファ上限

この章のポイント