2026年5月4日

この記事をシェア
- カテゴリ: セキュリティ / 暗号技術
- 対象読者: Web API、バックエンド、インフラを設計・実装するエンジニア
- SEOキーワード: AEADとは, Authenticated Encryption, AES-GCM, ChaCha20-Poly1305, nonce, AAD
1. 先に結論
NIST CSRCの用語集が定義するAEAD(Authenticated Encryption with Associated Data)は、暗号化した本文の秘匿性と、改ざんされていないことの検証を同じ暗号処理で実現する方式です。RFC 5116のAEADインターフェースでは、平文だけでなくAAD(関連データ)も認証対象にする抽象化が整理されています。日本語では「関連データ付き認証暗号」と説明されます。APIのトークン、Cookie、保存データ、メッセージキューのペイロードなど、「読めないようにしたい」だけでなく「書き換えられたら受け付けたくない」データに向いています。
実装時に最も重要なのは、アルゴリズム名を覚えることより、同じ鍵でnonceを再利用しないこと、復号前にタグを検証すること、AADを暗号化されないままでも認証対象に含めること、鍵をソースコードに埋め込まないことです。AES-GCMやChaCha20-Poly1305は広く使われていますが、使い方を誤ると安全性を失います。
2. AEADが解決する問題
暗号化だけでは改ざんを見抜けない
AES-CTRやストリーム暗号のように、平文と鍵ストリームをXORする方式は、内容を読めなくする一方で、暗号文のビットが変更されたときに受信側が必ず検出できるとは限りません。攻撃者が暗号文の一部を変更すると、対応する平文の一部も予測可能な形で変わる場合があります。暗号文をBase64にしても、Base64は表現形式であって暗号ではありません。
改ざん検知を別のMACで足す設計も可能ですが、暗号化・MACの順序、鍵の分離、検証前の復号、エラー処理を正しく設計する必要があります。AEADはこの組み合わせを標準化し、暗号文だけでなく認証対象のメタデータも一緒に検証します。
機密性、完全性、真正性
AEADが提供する中心的な性質は、暗号文から平文を推測しにくくする機密性と、暗号文・AADが変更されていれば復号に失敗させる完全性です。これはRFC 5116が整理する認証暗号のインターフェースで確認できます。認証タグが正しいことは「この鍵を持つ側が生成した、または少なくとも鍵に対応する計算結果である」ことを示します。ただし、AEAD単体で送信者の身元を証明する公開鍵署名になるわけではありません。鍵を共有している複数のサービスのうち、どれが生成したかまでは区別できません。
3. AAD、nonce、タグの役割
平文のAAD
AAD(Additional Authenticated Data)は、暗号化はしないが、改ざんは検出したいデータです。たとえばテナントID、メッセージ種別、プロトコルのバージョン、HTTPメソッドとパスなどが候補になります。AADは暗号文とは別に送信しますが、暗号化時と復号時に完全に同じバイト列を渡さなければなりません。
「JSONをAADにする」と決めた場合、空白やキー順の差が別のバイト列を生みます。文字列化の方法を固定するか、正規化された形式を使います。HTTPヘッダーをAADに入れるなら、大文字小文字、複数値、改行、プロキシによる追加・削除などを仕様に含めてください。曖昧なままヘッダー全体を認証対象にすると、正しいリクエストまで復号できなくなることがあります。
nonceは秘密ではないが一意でなければならない
nonce(number used once)は暗号処理ごとに使う値です。多くのAEADではnonceを暗号文と一緒に送信して構いません。秘密にする必要はありませんが、同じ鍵で同じnonceを再び使わないことが必要です。NIST SP 800-38DのGCM仕様はIV(nonce)の一意性と運用上の制約を説明しており、AES-GCMではnonce再利用が特に重大です。RFC 8439のChaCha20-Poly1305仕様でもnonceの再利用禁止を確認できます。
乱数nonceを使う場合も、乱数の品質と生成回数を考えます。大量の暗号化を一つの鍵で行うサービスでは、確率的衝突を無視できる期間か、カウンターを永続化できるかを確認します。プロセス再起動でカウンターが戻る実装は危険です。ライブラリがnonceを自動生成する場合でも、返されたnonceを保存し、復号側に渡せる形式で運びます。
認証タグ
タグは暗号文、AAD、nonce、鍵から計算される検証値です。一般的なAPIでは nonce、暗号文、タグを一つのバイナリやBase64文字列にまとめます。タグを短くすれば送信量は減りますが、偽造を偶然通過させる確率とのトレードオフです。タグ長や切り詰め可否はアルゴリズムとライブラリの仕様に従い、独自に短縮しないでください。
タグ検証に失敗したら、平文をアプリケーションへ渡してはいけません。失敗理由を「鍵が違う」「AADが違う」「暗号文が壊れた」と細かく外部へ返すと、攻撃者にヒントを与えることがあります。外向きには同じ認証失敗として扱い、内部ログには秘密情報や生の暗号文を残さない方針が安全です。
4. 代表的な方式
AES-GCM
AES-GCMはAESをブロック暗号として利用するGCMモードのAEADです。NIST SP 800-38DでGCMの認証暗号の構成、IV、タグ、運用上の制約を確認できます。CPUのAES命令を使える環境では高い性能を得やすく、TLSやクラウドの暗号サービスで広く採用されています。TLSで使う場合はプロトコルがnonceの構成を管理しますが、アプリケーションが直接使う場合はライブラリのAPIどおりにnonceを渡す必要があります。
GCMはnonceの一意性に非常に敏感です。固定nonceを「テストで動いたから」と本番へ持ち込むのは典型的な事故です。鍵ごとの暗号化回数、1回あたりの平文長、nonce生成方式を設計書に記録し、鍵をローテーションする条件も決めます。
ChaCha20-Poly1305
ChaCha20-Poly1305はChaCha20による暗号化とPoly1305による認証を組み合わせた方式です。RFC 8439でアルゴリズム、AAD、nonce、タグの形式と処理手順を確認できます。AESハードウェア支援がない環境や、モバイル・組み込み環境で性能を出しやすいという利点があります。TLS 1.3や多くの暗号ライブラリで利用できます。こちらもnonce再利用は禁止であり、「AES-GCMではないから安全」という意味ではありません。
どちらを選ぶかは、相互運用性、利用するライブラリ、ハードウェア支援、既存プロトコルの選択に合わせます。独自方式を設計するより、標準ライブラリが提供するAES-GCMまたはChaCha20-Poly1305を選ぶのが基本です。実装候補としては、Python cryptographyのAEAD API、libsodiumのAEAD API、OpenSSL EVPの暗号化APIのように、認証付き暗号を一つのインターフェースで扱える公式APIを優先します。複数言語で鍵や暗号文を共有する場合は、Google Tinkの公式リポジトリのような高水準ライブラリも選択肢です。
5. TLS、HTTP、APIでの使い分け
TLSは通信路を保護します。TLS 1.3の仕様(RFC 8446)では、ハンドシェイク、鍵スケジュール、レコード保護の役割が定義されています。ブラウザとサーバーの間の盗聴や改ざんを防ぎますが、TLS終端後にログ、キュー、データベースへ保存した値まで自動的に暗号化するわけではありません。逆に、アプリケーション層のAEADだけでTLSの代わりにしようとすると、接続認証、再送、証明書検証などを自作することになります。
APIでは、まずHTTPSを必須にし、そのうえで長期保存する個人情報や、複数の信頼境界を越えるペイロードをAEADで保護します。たとえば注文イベントに tenant_id と event_type をAADとして付ければ、暗号化された本文を別テナントの処理へ誤配送した際にも検出できます。ただし、AADは暗号化されないため、機密のテナント名を入れないでください。必要なら識別子を別途暗号化します。
HTTP Cookieへ入れる場合、Cookieの値を利用者が自由にコピーできること、サイズ制限、期限、失効、リプレイを考えます。AEADは改ざんを防ぎますが、盗まれた正しいCookieの再利用を自動で防止しません。短い有効期限、サーバー側セッション、失効リスト、デバイス結合などを組み合わせます。
6. Pythonによる実装例
実運用では、暗号ライブラリの低レベルAPIを直接組み合わせず、標準的なAEADインターフェースを使います。Python cryptographyのAESGCM APIでは、AADを渡して暗号化し、認証に失敗した場合は例外で扱う形を確認できます。以下は概念を示す例です。暗号文の形式は version || nonce || ciphertext_and_tag とし、Base64は輸送のためだけに使います。ライブラリによってタグが暗号文の末尾に連結される形式などが異なるため、実際には使用ライブラリの仕様を確認してください。
# 疑似コード。実際には cryptography.hazmat.primitives.ciphers.aead.AESGCM などを使用する
from secrets import token_bytes
from base64 import urlsafe_b64encode, urlsafe_b64decode
KEY = load_key_from_kms_or_secret_manager() # ソースコードやGitには置かない
def seal(plaintext: bytes, aad: bytes) -> str:
nonce = token_bytes(12) # ライブラリ推奨長を使用
ciphertext_and_tag = AESGCM(KEY).encrypt(nonce, plaintext, aad)
payload = b"v1." + nonce + ciphertext_and_tag
return urlsafe_b64encode(payload).decode("ascii")
def open_sealed(value: str, aad: bytes) -> bytes:
payload = urlsafe_b64decode(value.encode("ascii"))
if not payload.startswith(b"v1."):
raise InvalidMessage()
nonce = payload[3:15]
ciphertext_and_tag = payload[15:]
try:
return AESGCM(KEY).decrypt(nonce, ciphertext_and_tag, aad)
except InvalidTag:
raise InvalidMessage() # 平文は返さず、外部には同じエラー
\ntoken_bytes(12)を毎回呼べば十分という意味ではありません。鍵を複数プロセスで共有し、暗号化回数が極端に多い場合は、暗号ライブラリが推奨するnonce生成と鍵寿命の条件を確認します。nonceをデータベースの主キーにする、暗号文から生成する、固定値にする、といった独自工夫は避けます。
入力値のサイズ上限も設けます。巨大なリクエストを復号してから検査すると、メモリ枯渇を起こす可能性があります。バージョン、アルゴリズム識別子、鍵IDを暗号文の外側に置く場合は、それらをAADへ含めるか、タグの対象になる設計にしてください。鍵IDだけを信じて鍵を選ぶと、暗号文と鍵IDの組み合わせを差し替えられる余地が生まれます。
7. 鍵管理とローテーション
暗号鍵はパスワードやAPIキーと同じく、リポジトリ、Dockerイメージ、ログ、エラーメッセージに現れてはいけません。OWASP Cryptographic Storage Cheat Sheetも、鍵をデータと分離し、鍵管理とローテーションを設計する考え方を整理しています。KMS、HSM、クラウドのSecret Managerなどで保管し、アプリケーションには必要な権限だけを与えます。環境変数はソースコードよりましですが、プロセスダンプや運用画面に露出する場合があるため、万能な保管場所ではありません。
暗号文に鍵IDを添付するとローテーションが容易になります。復号は現行鍵と旧鍵を一定期間受け付け、再保存のタイミングで現行鍵へ更新します。旧鍵を即時削除すると、バックアップや障害復旧データを読めなくなるので、保持期間と廃棄手順を決めてから実施します。鍵へのアクセスを監査ログに残し、誰が読めるかを定期的に見直します。
8. 失敗時の扱いとテスト
タグ不一致、形式不正、未知のバージョン、期限切れ、鍵ID不明は、アプリケーション上の「無効なメッセージ」にまとめます。HTTP APIなら通常は認証失敗または不正入力として返し、500にして内部実装を露出させないようにします。リトライしてもタグ不一致は直らないため、メッセージキューでは無限リトライを避け、隔離キューへ送る設計が必要です。
テストでは、正常復号だけでなく、nonceの1ビット変更、AADの変更、暗号文の変更、タグの変更、切り詰め、順序変更、未知のバージョンを確認します。同じ鍵・同じnonceで二度暗号化しないことをテスト環境の監視で検出します。プロセス再起動、時刻ずれ、鍵ローテーション中、バックアップ復元後もテスト対象です。ログに平文・鍵・完全なトークンが出ていないことも確認します。
9. よくある質問
AEADがあればHTTPSは不要ですか?
不要にはなりません。AEADは特定のデータを保護する仕組みであり、TLSが提供する通信路の認証や接続全体の保護を代替しません。通常はHTTPSを使い、追加の保存・転送要件にAEADを使います。
nonceは暗号文と一緒に送ってよいですか?
一般的なAEADでは送って構いません。秘密性ではなく、同じ鍵での再利用禁止が要件です。送信形式にnonceを必ず含め、復号時に正しい長さとバージョンを検証します。
パスワードをAEADで暗号化して保存できますか?
パスワードは通常、復号する必要がないためAEADではなく、Argon2id、scrypt、bcryptなどのパスワードハッシュを使います。復号が必要なAPI資格情報や外部連携秘密情報とは目的が異なります。
暗号文をBase64にすれば安全ですか?
Base64はバイナリを文字列へ変換するだけです。秘匿性も改ざん検知もありません。AEADの出力を輸送するために使い、必ずHTTPSやアクセス制御と組み合わせます。
10. まとめ:実践チェックリスト
- AES-GCMまたはChaCha20-Poly1305を、実績のあるライブラリで使う
- 同じ鍵でnonceを再利用しない。再起動・複数プロセス時の設計を確認する
- AADのバイト列表現を仕様化し、機密情報はAADに置かない
- タグ検証が成功するまで平文を業務処理へ渡さない
- 鍵をKMS等で管理し、鍵ID・ローテーション・旧鍵保持期間を決める
- 盗まれた正規トークンのリプレイ対策を別途設計する
- 失敗理由を外部へ詳しく返さず、ログにも秘密を残さない
- 改ざん、形式不正、復旧、ローテーションをテストする
- 通信はTLS、パスワードは専用ハッシュという責務分担を守る
11. 実装前に決めるデータ形式
暗号化処理を導入する前に、データ形式を小さな仕様書にします。最低限、バージョン、アルゴリズム、鍵ID、nonce、暗号文、タグ、AADの意味、文字コード、サイズ上限を記録します。フィールドを個別にBase64化するのか、全体を一つのBase64へ包むのかも固定します。JSONを使う場合は、数値と文字列の型、NULL、Unicode正規化、改行を曖昧にしません。
復号の互換性も先に決めます。新しい鍵で生成したv2を旧サービスが読めないなら、切り替えを同時に行うか、旧サービスがv1を読む期間を設けます。未知のバージョンを「たぶんv1」として解釈する実装は、別の形式を誤って受け入れるため避けます。入力の長さをタグ検証より前に上限チェックし、巨大な値や異常に長い鍵IDでリソースを消費しないようにします。
監査では、暗号方式を使っている事実だけでなく、鍵の所有者、ローテーション担当、障害時の連絡先、データ削除時の鍵廃棄、テスト用鍵の扱いを確認します。暗号化によって検索や集計が難しくなる場合は、検索用の非機密な識別子、トークン化、アクセス制御との組み合わせを検討し、復号できる人を増やす設計で解決しないようにします。
12. 参考資料
- RFC 5116: An Interface and Algorithms for Authenticated Encryption — AEADのインターフェース、AAD、暗号化・復号の抽象的な入出力を確認できます。
- NIST SP 800-38D: Recommendation for GCM — GCMの仕様、IV(nonce)、認証タグ、運用上の制約を確認できます。
- RFC 8439: ChaCha20 and Poly1305 for IETF Protocols — ChaCha20-Poly1305の処理手順、nonce、AAD、タグの形式を確認できます。
- NIST CSRC Glossary: Authenticated Encryption with Associated Data — AEADという用語の定義を確認できます。
- Python cryptography AEAD API — AESGCMやChaCha20Poly1305の公式APIと認証失敗時の扱いを確認できます。
- libsodium: AEAD constructions — nonce、AAD、暗号文・タグを扱う高水準APIを確認できます。
- OpenSSL EVP_EncryptInit documentation — EVPの暗号化初期化APIとAEAD制御用の仕様を確認できます。
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3 — TLS 1.3の通信路保護とレコード暗号化の役割を確認できます。
- OWASP Cryptographic Storage Cheat Sheet — 鍵の分離、保管、ローテーションなどの運用原則を確認できます。
- Google Tink公式リポジトリ — 独自暗号の組み合わせを避ける高水準暗号ライブラリの実装を確認できます。
