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_PKCS7 | CBC。前のブロックの暗号文を次の平文と XOR。PKCS#7 で末尾を詰める | 任意(暗号文は 16 の倍数に切り上げ) | 16 バイト。予測不能であること | 古いプロトコルの互換 |
PSA_ALG_CBC_NO_PADDING | CBC。詰め物なし | 16 の倍数のみ | 16 バイト | 固定長データ |
PSA_ALG_ECB_NO_PADDING | ブロックごとに独立に暗号化 | 16 の倍数 | なし | データ暗号化には使わない。鍵の包み込みなどの部品専用 |
PSA_ALG_CFB / PSA_ALG_OFB | 古いストリーム化モード | 任意 | 16 バイト | 互換 |
PSA_ALG_XTS | ストレージ暗号化用 | 16 以上 | 16 バイト(セクタ番号) | ディスク暗号化 |
PSA_ALG_STREAM_CIPHER | ChaCha20(鍵種別 PSA_KEY_TYPE_CHACHA20) | 任意 | 12 バイトのノンス | AEAD の内部 |
新規設計で選ぶのは CTR だけでよい。 CBC は「詰め物の検証結果が漏れると平文が復元できる」パディングオラクル攻撃の温床であり、ECB は同じ平文ブロックが同じ暗号文になるため画像の輪郭がそのまま見えるほど弱い。
2. IV — 一度きりの初期値
CTR も CBC も、IV(Initialization Vector、初期化ベクトル)という 16 バイトの値を必要とする。 IV は秘密ではない(暗号文と一緒に送ってよい)が、同じ鍵で同じ IV を二度使ってはいけない。
- CTR で IV が繰り返されると、2 つの暗号文の XOR が 2 つの平文の XOR になり、片方の平文が分かればもう片方が丸見えになる。鍵そのものが漏れたのと同等の被害
- CBC で IV が予測可能だと、選択平文攻撃(攻撃者が平文を選んで暗号化させ、比較する攻撃)が可能になる
PSA は、この「二度使わない」をライブラリが乱数で IV を作ることで守らせる。 一発関数 psa_cipher_encrypt() は IV を自動生成し、出力の先頭に IV を付けて返す。 これが、この章で最も重要な規則である。
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_cipher_encrypt(): IV を乱数で生成し、出力 = IV ∥ 暗号文。出力サイズはPSA_CIPHER_ENCRYPT_OUTPUT_SIZE(key_type, alg, input_length)psa_cipher_decrypt(): 入力の先頭を IV として読む。出力 = 平文。出力サイズはPSA_CIPHER_DECRYPT_OUTPUT_SIZE(key_type, alg, input_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、STREAM | IV 長 + L | L − IV 長 |
| CBC_PKCS7 | IV 長 + (L を 16 の倍数に切り上げ + 16 の倍数ちょうどのときはさらに 16) | L − IV 長(実長はこれ以下) |
| CBC_NO_PADDING、ECB | IV 長 + 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 は溜めないので、常に入力長 = 出力長である。
分割版で 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. なぜ単独で使ってはいけないのか
対称暗号は機密性(内容を読まれない)だけを守り、完全性(改ざんされていない)は守らない。
- CTR では、暗号文の任意のビットを反転すると、平文の同じ位置のビットが反転する。攻撃者は平文を知らなくても「送金額の桁」を狙って書き換えられる
- CBC では、IV や前ブロックをいじることで、次ブロックの平文を操作できる
- そして 3 節で述べたように、復号は失敗しない。ゴミが出ても、それがゴミだと分かるのはアプリケーション側だけである
「暗号文の後ろに MAC を付ける」(Encrypt-then-MAC)で解決できるが、鍵を 2 つ管理し、順序を間違えず、MAC の対象に IV を含める、といった落とし穴が多い。 それを 1 つの操作にまとめ、復号時に改ざんを検出して失敗させるのが、次章の AEAD である。
実務では、「データを暗号化する」と言われたら AEAD を使い、psa_cipher_* はプロトコルが CTR や CBC 単体を要求する場合の互換用と考える。
7. 手を動かす
モードと IV を確かめる
8. 仕様書に逃がす
| 関数・定数 | 用途 |
|---|---|
PSA_ALG_CCM_STAR_NO_TAG | IEEE 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 で追加) |
この章のポイント
- 対称暗号 = 同じ鍵で暗号化と復号。モードは CTR を選ぶ
- 一発関数の出力は IV ∥ 暗号文。復号は先頭 16 バイトを IV として読む
- 分割関数では IV を別に扱う: 暗号化は
generate_iv、復号はset_iv。updateの前に 1 回 - 同じ鍵で同じ IV は二度使わない。乱数 IV をライブラリに任せるのが最も安全
- 対称暗号は改ざんを検出できず、復号は失敗しない。データの暗号化には AEAD(次章)を使う