PSA APIs 07 · 対称暗号 — psa_cipher で AES を使う

Chapter 07

対称暗号 — psa_cipher で AES を使う

この章のゴール.

psa_cipher_encrypt() / psa_cipher_decrypt() の出力に IV が含まれるという規則、 モード(CTR・CBC)ごとの制約、分割版の generate_iv / set_iv の使い分けを理解し、 そして「これを単独で使ってはいけない理由」を説明できるようになること。

この章で使う既出の用語(定義は各リンク先). PSA(01 章 1 節)、サイズマクロ(02 章 4 節)、ビット(02 章 4 節)、algorithm(03 章 11 節)、type(03 章 11 節)、usage(03 章 11 節)、generate(04 章 10 節)、乱数(05 章 1 節)、MAC(06 章 1 節)

1. 対称暗号と「モード」

対称暗号(symmetric cipher)は、同じ鍵で暗号化と復号を行う暗号である。 代表は AES(Advanced Encryption Standard)で、16 バイトのブロックを 16 バイトの暗号文に変換するブロック暗号である。

16 バイトより長いデータを扱うには、ブロックをどうつなぐかというモード(mode of operation)が必要になる。 PSA の psa_cipher_* 関数群は、「認証なしの暗号化モード」を担当する。

アルゴリズム定数方式入力長の制約IV用途
PSA_ALG_CTRカウンタモード。カウンタを暗号化した列を平文と XOR任意(暗号文長 = 平文長)16 バイト。同じ鍵で二度と繰り返さないディスク・メモリ暗号化、AEAD の内部
PSA_ALG_CBC_PKCS7CBC。前のブロックの暗号文を次の平文と XOR。PKCS#7 で末尾を詰める任意(暗号文は 16 の倍数に切り上げ)16 バイト。予測不能であること古いプロトコルの互換
PSA_ALG_CBC_NO_PADDINGCBC。詰め物なし16 の倍数のみ16 バイト固定長データ
PSA_ALG_ECB_NO_PADDINGブロックごとに独立に暗号化16 の倍数なしデータ暗号化には使わない。鍵の包み込みなどの部品専用
PSA_ALG_CFB / PSA_ALG_OFB古いストリーム化モード任意16 バイト互換
PSA_ALG_XTSストレージ暗号化用16 以上16 バイト(セクタ番号)ディスク暗号化
PSA_ALG_STREAM_CIPHERChaCha20(鍵種別 PSA_KEY_TYPE_CHACHA20)任意12 バイトのノンスAEAD の内部

新規設計で選ぶのは CTR だけでよい。 CBC は「詰め物の検証結果が漏れると平文が復元できる」パディングオラクル攻撃の温床であり、ECB は同じ平文ブロックが同じ暗号文になるため画像の輪郭がそのまま見えるほど弱い。

ブロック暗号の「モード」— 16 バイトのブロックをどうつなぐかECB(使わない)P1P2P1AES(K)C1C2C1同じ平文 → 同じ暗号文。模様が見えるCBC(互換のみ)IVP1P2IV⊕P1 → AES → C1、C1⊕P2 → AES → C2IVC1C2前の暗号文が次に混ざる。詰め物が要るCTR(標準)IV∥0IV∥1IV∥2AES(K, カウンタ) ⊕ P → CC1C2C3任意長。並列可。詰め物なしどのモードも「認証」はしない暗号文を書き換えても復号は失敗せず、ゴミか、狙った位置のビット反転が出る。データの暗号化には AEAD(8 章)を使うpsa_cipher_* は、プロトコルが CTR / CBC 単体を要求する場合の互換用と考える
ECB は模様が残り、CBC は詰め物と連鎖の問題を持つ。新規設計で選ぶのは CTR だけ

2. IV — 一度きりの初期値

CTR も CBC も、IV(Initialization Vector、初期化ベクトル)という 16 バイトの値を必要とする。 IV は秘密ではない(暗号文と一緒に送ってよい)が、同じ鍵で同じ IV を二度使ってはいけない。

PSA は、この「二度使わない」をライブラリが乱数で IV を作ることで守らせる。 一発関数 psa_cipher_encrypt() は IV を自動生成し、出力の先頭に IV を付けて返す。 これが、この章で最も重要な規則である。

一発関数の出力は「IV ∥ 暗号文」psa_cipher_encrypt(key, CTR, plain[50]) の出力(66 バイト)IV(16 バイト、ライブラリが乱数で生成)ct[0..15]暗号文(50 バイト)ct[16..65]psa_cipher_decrypt(key, CTR, ct[66]) は先頭 16 バイトを IV として読み、残りを復号 → 50 バイトIV は秘密ではないが、同じ鍵で二度使ってはいけないCTR で IV が繰り返されると 2 つの平文の XOR が漏れる。乱数 IV をライブラリに任せるのが最も安全
出力サイズは PSA_CIPHER_ENCRYPT_OUTPUT_SIZE で計算する。IV の 16 バイトを忘れると BUFFER_TOO_SMALL

3. 一発関数 — 出力は「IV ∥ 暗号文」

psa_status_t psa_cipher_encrypt(psa_key_id_t key, psa_algorithm_t alg,
                                const uint8_t *input, size_t input_length,
                                uint8_t *output, size_t output_size, size_t *output_length);

psa_status_t psa_cipher_decrypt(psa_key_id_t key, psa_algorithm_t alg,
                                const uint8_t *input, size_t input_length,
                                uint8_t *output, size_t output_size, size_t *output_length);
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attr, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attr, 128);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attr, PSA_ALG_CTR);
PSA_CHECK(psa_generate_key(&attr, &key));

uint8_t plain[50] = "...";
uint8_t ct[PSA_CIPHER_ENCRYPT_OUTPUT_SIZE(PSA_KEY_TYPE_AES, PSA_ALG_CTR, 50)];  /* 16 + 50 = 66 */
size_t ct_len;
PSA_CHECK(psa_cipher_encrypt(key, PSA_ALG_CTR, plain, 50, ct, sizeof ct, &ct_len));
/* ct[0..15] が IV、ct[16..65] が暗号文。ct_len == 66 */

uint8_t back[50]; size_t back_len;
PSA_CHECK(psa_cipher_decrypt(key, PSA_ALG_CTR, ct, ct_len, back, sizeof back, &back_len));
/* back_len == 50 */

復号側が「暗号文の先頭 16 バイトが IV である」と知らずに、暗号文だけを渡すと、 先頭 16 バイトの暗号文が IV として解釈され、残りが復号されてゴミが出る。エラーにはならない(暗号化には「正しさ」の概念がない)。 これが、この章の最後で述べる「対称暗号を単独で使ってはいけない」理由の 1 つでもある——復号は失敗しないのである。

出力サイズマクロの中身は次の通りで、モードによって違う。

モード暗号化出力サイズ(input_length = L)復号出力サイズ
CTR、CFB、OFB、STREAMIV 長 + LL − IV 長
CBC_PKCS7IV 長 + (L を 16 の倍数に切り上げ + 16 の倍数ちょうどのときはさらに 16)L − IV 長(実長はこれ以下)
CBC_NO_PADDING、ECBIV 長 + L(L は 16 の倍数)L − IV 長

4. 分割関数 — IV を自分で扱う

psa_cipher_operation_t op = PSA_CIPHER_OPERATION_INIT;
psa_status_t psa_cipher_encrypt_setup(psa_cipher_operation_t *op, psa_key_id_t key, psa_algorithm_t alg);
psa_status_t psa_cipher_decrypt_setup(psa_cipher_operation_t *op, psa_key_id_t key, psa_algorithm_t alg);
psa_status_t psa_cipher_generate_iv(psa_cipher_operation_t *op, uint8_t *iv, size_t iv_size, size_t *iv_length);
psa_status_t psa_cipher_set_iv(psa_cipher_operation_t *op, const uint8_t *iv, size_t iv_length);
psa_status_t psa_cipher_update(psa_cipher_operation_t *op, const uint8_t *input, size_t input_length,
                               uint8_t *output, size_t output_size, size_t *output_length);
psa_status_t psa_cipher_finish(psa_cipher_operation_t *op, uint8_t *output, size_t output_size, size_t *output_length);
psa_status_t psa_cipher_abort(psa_cipher_operation_t *op);

分割版では、IV は出力に含まれず、別に扱う。 setup の直後に、暗号化なら psa_cipher_generate_iv()(ライブラリが作って返す)、 復号なら psa_cipher_set_iv()(受け取った IV を渡す)を呼ぶ。 どちらも update の前に 1 回だけ。順序を違えると PSA_ERROR_BAD_STATE。

/* 暗号化 */
psa_cipher_operation_t op = PSA_CIPHER_OPERATION_INIT;
uint8_t iv[PSA_CIPHER_IV_LENGTH(PSA_KEY_TYPE_AES, PSA_ALG_CTR)];   /* 16 */
size_t iv_len, n, total = 0;

PSA_CHECK(psa_cipher_encrypt_setup(&op, key, PSA_ALG_CTR));
PSA_CHECK(psa_cipher_generate_iv(&op, iv, sizeof iv, &iv_len));   /* IV を別途送る */
for (each chunk) {
    PSA_CHECK(psa_cipher_update(&op, chunk, chunk_len, out + total, out_size - total, &n));
    total += n;      /* n は chunk_len と同じとは限らない(CBC はブロック単位で溜める) */
}
PSA_CHECK(psa_cipher_finish(&op, out + total, out_size - total, &n));   /* CBC なら詰め物の最終ブロック */
total += n;

/* 復号 */
PSA_CHECK(psa_cipher_decrypt_setup(&op, key, PSA_ALG_CTR));
PSA_CHECK(psa_cipher_set_iv(&op, iv, iv_len));
...

update の出力長は入力長と一致しないことがある。 CBC は 16 バイト溜まるまで出力しないので、10 バイト入れても出力は 0 で、次に 10 バイト入れると 16 バイト出る。 出力バッファは PSA_CIPHER_UPDATE_OUTPUT_SIZE(key_type, alg, input_length) と PSA_CIPHER_FINISH_OUTPUT_SIZE(key_type, alg) で見積もる。 CTR は溜めないので、常に入力長 = 出力長である。

分割関数の順序 — IV は update の前に 1 回encrypt_setup / decrypt_setup鍵とモードを決めるgenerate_iv(暗号化) / set_iv(復号)update の前に 1 回だけupdate × nCBC は 16 バイト溜まるまで出力しないfinishCBC の詰め物の最終ブロックupdate の出力長 ≠ 入力長のことがあるCBC: 10 バイト入れて 0 バイト出て、次に 10 バイトで 16 出る。CTR: 常に同じ長さset_iv を暗号化側に使うならIV が二度使われないことを自分で保証する。再起動でカウンタが戻る設計は典型的な失敗バッファは PSA_CIPHER_UPDATE_OUTPUT_SIZE / PSA_CIPHER_FINISH_OUTPUT_SIZE で見積もる
分割版では IV が出力に含まれず、別に扱う

分割版で set_iv を暗号化側に使うこともできる。 「IV をカウンタで管理して、乱数を使わない」設計(各メッセージにフレーム番号を IV として使う CTR)などである。 ただし、その IV が二度使われないことを自分で保証する責任が生じる。 電源断のたびにカウンタが 0 に戻る設計は典型的な失敗である。

5. 鍵の包み込み(key wrapping)

対称暗号の正当な単独用途の 1 つが、別の鍵を暗号化して保存・転送する「鍵の包み込み」である。 ただし PSA では、これもAEAD で行うのが原則である(改ざんされた鍵を復号して使う事故を防ぐ)。

ECB を使う「AES Key Wrap(鍵の包み込み専用の方式。RFC 3394)」は、PSA Crypto API 1.3 で psa_wrap_key() / psa_unwrap_key() として追加された。 対応実装は少ないので、18 章の「仕様書に逃がす」で触れる。

6. なぜ単独で使ってはいけないのか

対称暗号は機密性(内容を読まれない)だけを守り、完全性(改ざんされていない)は守らない。

「暗号文の後ろに MAC を付ける」(Encrypt-then-MAC)で解決できるが、鍵を 2 つ管理し、順序を間違えず、MAC の対象に IV を含める、といった落とし穴が多い。 それを 1 つの操作にまとめ、復号時に改ざんを検出して失敗させるのが、次章の AEAD である。

実務では、「データを暗号化する」と言われたら AEAD を使い、psa_cipher_* はプロトコルが CTR や CBC 単体を要求する場合の互換用と考える。

CTR のビット反転 — 復号は失敗しない平文 P金額 = 100暗号文 C の 1 ビットを攻撃者が反転C ⊕ 00…04…00復号結果金額 = 104暗号文の同じ位置の平文ビットが反転する攻撃者は平文の中身を知らなくても「桁の位置」を狙って書き換えられる。psa_cipher_decrypt は PSA_SUCCESS を返し、アプリはゴミを正しい値だと思って使う
機密性は守っても完全性は守らない。だから AEAD が要る

7. 手を動かす

モードと IV を確かめる

8. 仕様書に逃がす

関数・定数用途
PSA_ALG_CCM_STAR_NO_TAGIEEE 802.15.4(Zigbee などが使う低消費電力無線の規格)用の、タグなし CCM。無線スタックの内部
PSA_ALG_XTSディスク暗号化専用モード
PSA_BLOCK_CIPHER_BLOCK_LENGTH(type)ブロック長(AES は 16)
PSA_CIPHER_IV_MAX_SIZE、PSA_CIPHER_UPDATE_OUTPUT_MAX_SIZE実行時にアルゴリズムが決まるときの上限
psa_wrap_key() / psa_unwrap_key()鍵の包み込み(1.3 で追加)

この章のポイント