暗号と鍵の扱い
この部の 4 / 8 章 ・ 全体で 65 / 76 章 ・ 読了目安 45 分
- 守りたい性質から手段を選べる
- 鍵をローテーションできる設計にできる
- 定数時間比較の必要性を説明できる
HTTPS、パスワードのハッシュ、Webhook の署名検証、JWT—— ここまでの章で、暗号は何度も出てきました。
ただし共通して書いてきたのは「自作しない」ということだけです。 この章では、その中身を最低限だけ整理します。
目的は実装できるようになることではなく、正しく選び、正しく運用するためです。
暗号アルゴリズム自体は、論文として公開されています。 実装するだけなら難しくありません。
問題は、正しく動いているように見えて、破れる実装が簡単に作れることです。
□ 暗号化して、復号できた → 正しく動いているように見える
□ しかし、鍵の使い方が悪くて、実は解読できる
□ しかも、テストでは絶対に検出できない
普通のバグは動作で気づけます。暗号のバグは、破られるまで気づけません。 だから「実績のあるライブラリを、正しい使い方で使う」以外の選択肢がありません。
暗号が提供する4つのこと
混同しやすいので、最初に分けます。
| 性質 | 意味 | 主な手段 |
|---|---|---|
| 機密性 | 他人に読まれない | 暗号化 |
| 完全性 | 改ざんされていない | ハッシュ・MAC・署名 |
| 認証 | 相手が本人である | 署名・証明書・MAC |
| 否認防止 | 「送っていない」と言わせない | デジタル署名 |
「暗号化すれば安全」ではありません。 何を守りたいのかで手段が変わります。
ハッシュ関数
入力(任意の長さ) → ハッシュ関数 → 固定長の値
□ 同じ入力からは、必ず同じ出力
□ 出力から入力は復元できない(一方向)
□ 違う入力が同じ出力になりにくい(衝突耐性)
用途は改ざん検知と同一性の判定です。
□ ファイルが壊れていないかの確認
□ 内容が変わったかの判定(キャッシュキー、Git のコミット ID)
□ パスワードの保存 ← ただし専用のものを使う(後述)
使ってよい: SHA-256, SHA-3, BLAKE2/3
使わない: MD5, SHA-1(衝突が現実的に作れる)
✕ 「パスワードを暗号化して保存しています」(実際はハッシュ化)
暗号化は元に戻せます。ハッシュは戻せません。 パスワードを「元に戻せる形」で保存しているなら、それ自体が問題です。
そして認証と認可の実装のとおり、パスワードには SHA-256 ではなく bcrypt / argon2 / scrypt を使います。 これらは意図的に遅く作られており、総当たりを実用的でなくします。
HMAC — 鍵付きハッシュ
「この内容を、鍵を知っている人が作った」ことを示せます。
送信側: HMAC(鍵, 本文) を計算し、本文と一緒に送る
受信側: 同じ鍵で計算し、一致するか確認する
間違えられない処理を作るの Webhook の署名検証が、これです。
mac := hmac.New(sha256.New, secretKey)
mac.Write(body)
expected := mac.Sum(nil)
if !hmac.Equal(expected, received) { // ← 単純な比較を使わない(後述)
return ErrInvalidSignature
}鍵を共有している者同士でしか使えませんが、単純で高速です。
共通鍵暗号と公開鍵暗号
| 共通鍵(対称) | 公開鍵(非対称) | |
|---|---|---|
| 鍵 | 同じ鍵で暗号化・復号 | 公開鍵で暗号化、秘密鍵で復号 |
| 速度 | 速い | 遅い |
| 課題 | 鍵をどう渡すか | 遅い |
| 代表 | AES, ChaCha20 | RSA, ECDSA, Ed25519 |
公開鍵暗号は「鍵をどう渡すか」を解決するために生まれました。 公開鍵は誰に見られても構わないので、事前の共有が要りません。
TLS は両方を使う
1. 公開鍵暗号で、共通鍵を安全に共有する(遅いが、短いデータだけ)
2. 以降の通信は共通鍵で暗号化する(速い)
DNS・TCP・TLSの TLS ハンドシェイクは、これをやっています。 「遅いが鍵配送を解決できる方式」で「速い方式の鍵」を渡す—— これがハイブリッド暗号の考え方です。
デジタル署名
公開鍵暗号を逆向きに使います。
署名: 秘密鍵で作る → 秘密鍵を持つ本人しか作れない
検証: 公開鍵で確認する → 誰でも検証できる
これにより、
□ 誰が作ったかが分かる(認証)
□ 改ざんされていないことが分かる(完全性)
□ 「作っていない」と言えない(否認防止) ← HMAC には無い性質
JWT の署名(認証と認可の実装)、証明書、パッケージの署名が、これです。
HMAC 鍵を共有している2者間。速い。第三者は検証できない
署名 公開鍵を配れば誰でも検証できる。第三者に証明できる
Webhook(自社と決済会社) → HMAC で十分
JWT を複数サービスが検証する → 署名(公開鍵)が向く
証明書と認証局
公開鍵には問題があります。その公開鍵が本物か、どう確かめるのか。
攻撃者が「これが example.com の公開鍵です」と偽物を渡したら?
そこで、信頼できる第三者(認証局・CA)が保証します。
証明書 = 公開鍵 + 所有者情報 + CA の署名
ブラウザや OS は、信頼する CA の一覧を最初から持っています。 だから証明書チェーンをたどって検証できます(つながらない時に何を見るか)。
□ 期限切れ → 検証に失敗する
□ 名前不一致 → SAN にホスト名が含まれていない
□ 中間証明書の不足 → ブラウザでは通るのに curl で失敗する(つながらない時に何を見るか)
□ 自己署名 → CA の保証が無いので、警告が出る
乱数
セキュリティ用途では、専用の乱数生成器を使います。
Math.random() // ✕ 予測可能。トークン生成に使わない
crypto.randomUUID() // ○
crypto.getRandomValues(new Uint8Array(32)) // ○math/rand // ✕
crypto/rand // ○□ セッション ID
□ パスワードリセットのトークン
□ 招待コード
□ 署名付き URL のキー(ファイルとストレージ)
これらに通常の乱数を使うと、次の値を予測される可能性があります。
「ランダムに見える」ことと「予測できない」ことは、まったく違います。
鍵の管理 — 実務ではここが本体
アルゴリズムより、鍵の扱いのほうが事故が多いです。
□ コードに書かない・git にコミットしない(セキュリティ)
□ 環境変数か、専用のサービス(Secret Manager / KMS / Vault)で管理する
□ Terraform の state に平文で入らないよう注意する(Terraform と Infrastructure as Code)
□ 権限を絞る(誰がその鍵を読めるか = 誰がその暗号を破れるか)
□ ログに出さない
ローテーション
□ 定期的に鍵を交換する
□ 交換の手順を、あらかじめ用意しておく
□ 「古い鍵と新しい鍵の両方を受け付ける期間」を設ける
✕ 鍵が1つしかなく、切り替えると全ユーザーがログアウトする
✕ 過去のデータが、その鍵でしか復号できない
鍵は「いつか漏れるもの」として設計します。
□ 鍵に ID を付け、どの鍵で暗号化したかをデータと一緒に保存する
□ 検証は複数の鍵で試せるようにする(新旧の並行期間)
漏洩が発覚してから「切り替えられません」となるのが、最悪の状態です。
漏洩した時
1. すぐに報告する(自分で判断しない。ルールを守る — 情報を扱う者の日常)
2. 鍵を無効化・ローテーションする
3. その鍵で何ができたかを洗い出す
4. 影響範囲を特定する(アクセスログ)
5. 必要なら報告義務を確認する(エンジニアと法律)
「コミットしてしまったが、すぐ消したから大丈夫」ではありません。 git の履歴には残り、公開リポジトリなら数秒でクロールされています。 必ず無効化してください。
よくある間違い
| 間違い | 何が起きるか |
|---|---|
| 自作の暗号 | ほぼ確実に破れる |
| Base64 を暗号だと思う | ただの符号化。誰でも戻せる(認証と認可の実装の JWT) |
| ECB モードを使う | 同じ平文が同じ暗号文になり、模様が見える |
| IV / nonce を使い回す | 解読の手がかりになる |
署名の比較に == を使う | タイミング攻撃の対象になる |
| 鍵をハードコード | リポジトリから漏れる |
Math.random() でトークン生成 | 予測される |
| 古いアルゴリズム(MD5, SHA-1, DES, RC4) | 既に破られている |
if signature == expected { ... } // ✕ 一致する文字数で処理時間が変わる
if hmac.Equal(signature, expected) { } // ○ 常に同じ時間で比較する比較を途中で打ち切ると、「何文字目まで合っていたか」が 処理時間から推測できます。1文字ずつ総当たりされると、 現実的な回数で正解にたどり着かれます。
署名やトークンの比較には、必ず定数時間比較の関数を使ってください。
実務での判断
新人が判断する場面は、実はそれほど多くありません。
□ パスワードを保存する → bcrypt / argon2(自分で考えない)
□ トークンを生成する → 暗号論的乱数
□ Webhook を検証する → HMAC + 定数時間比較
□ 通信を守る → TLS(自分で暗号化しない)
□ 保存データを暗号化したい → クラウドの標準機能 / KMS を検討する
□ 独自の暗号方式が必要 → **その判断自体を相談する**
「暗号を自分で設計している」と感じたら、それは止まるべき合図です。
Webhook の署名を検証するコードで、`if receivedSig == expectedSig` と書かれていました。何を指摘しますか。
実務の落とし穴まとめ
- 暗号を自作する — 動いているように見えて破れる
- ハッシュを暗号化と呼ぶ — 戻せるかどうかが根本的に違う
- パスワードを SHA-256 で保存 — 専用の関数を使う(認証と認可の実装)
Math.random()でトークン生成 — 予測される- 署名の比較に
==— タイミング攻撃 - 鍵をコードに書く / git にコミット — 即座にクロールされる
- ローテーションできない設計 — 漏洩時に切り替えられない
- Base64 を暗号と勘違い — 誰でも戻せる
- 古いアルゴリズムを使う — MD5 / SHA-1 / DES は使わない
まとめ
- 暗号は機密性・完全性・認証・否認防止のどれを守るかで手段が変わる
- ハッシュは戻せない(改ざん検知・同一性判定)。 パスワードには bcrypt / argon2
- HMAC は鍵を共有する2者間、署名は第三者にも検証できる
- 公開鍵暗号は鍵配送を解決するために生まれた。TLS は両方を組み合わせる
- 証明書は「公開鍵が本物か」を CA が保証する仕組み
- トークンには暗号論的乱数を使う
- 実務の本体は鍵の管理。コードに書かず、権限を絞り、 ローテーションできる設計にする
- 署名の比較は定数時間比較
- 「暗号を設計している」と感じたら、止まって相談する
公式ドキュメント・参考資料
| 対象 | リンク |
|---|---|
| OWASP 暗号のチートシート | https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html |
| IPA 暗号技術 | https://www.ipa.go.jp/security/crypto/ |
| CRYPTREC(推奨暗号リスト) | https://www.cryptrec.go.jp/ |
| Go の crypto パッケージ | https://pkg.go.dev/crypto |
| MDN Web Crypto API | https://developer.mozilla.org/ja/docs/Web/API/Web_Crypto_API |
CRYPTREC は「どのアルゴリズムを使ってよいか」の日本での基準です。 迷ったらここを見ると、選択の根拠になります。
次の章では、これらの防御をいつ・どう設計に組み込むかを扱います。