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)バイト |
| 典型的用途 | 通信フレームの認証 | ファームウェアの検証、装置の認証、証明書 |
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_EDDSA | ECC_KEY_PAIR(TWISTED_EDWARDS) 255 ビット | 64 | Ed25519。決定論的で高速、実装の落とし穴が少ない。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_ANY | ECC | 64 | ハッシュを指定しない。既にハッシュ済みのデータを 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_hash | sign_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 は「宣言と違う使い方」を禁止する。
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 か」を確かめる手順である。
- サーバが乱数(チャレンジ、32 バイト)を送る
- 装置が秘密鍵でチャレンジに署名して返す
- サーバが登録済みの公開鍵で検証する
装置側:
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 章のアテステーションはこの問題を構造的に解決する)。
6. 署名のバイト列形式
署名の形式は、他のライブラリと相互運用するときに必ず躓く点である。
| アルゴリズム | PSA の形式 | 他で見る形式 |
|---|---|---|
| ECDSA | r ∥ 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 バイト)。変換が必要 |
| Ed25519 | R ∥ 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 以降)。
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 | アルゴリズムが実行時に決まるときのバッファ上限 |
この章のポイント
- 署名は秘密鍵で作り、公開鍵で検証。検証者は偽造できない(MAC との決定的な違い)
- ECDSA P-256 + SHA-256 または Ed25519 を選ぶ。乱数に不安があれば
DETERMINISTIC_ECDSA sign_hashは自分でハッシュを計算して渡す(大きなデータ向け)。sign_messageは内部でハッシュ。Ed25519 は message 版のみ- ハッシュの長さはアルゴリズム定数のハッシュと一致させる
- ECDSA の署名は r ∥ s の 64 バイト。DER と相互変換が必要な場面が多い
- 検証用の公開鍵は攻撃者に差し替えられない場所に置く