プログラマのための IT 教科書

暗号と鍵の扱い

この部の 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, ChaCha20RSA, ECDSA, Ed25519

公開鍵暗号は「鍵をどう渡すか」を解決するために生まれました。 公開鍵は誰に見られても構わないので、事前の共有が要りません。

TLS は両方を使う

1. 公開鍵暗号で、共通鍵を安全に共有する(遅いが、短いデータだけ)
2. 以降の通信は共通鍵で暗号化する(速い)

DNS・TCP・TLSの TLS ハンドシェイクは、これをやっています。 「遅いが鍵配送を解決できる方式」で「速い方式の鍵」を渡す—— これがハイブリッド暗号の考え方です。

デジタル署名

公開鍵暗号を逆向きに使います。

署名: 秘密鍵で作る    → 秘密鍵を持つ本人しか作れない
検証: 公開鍵で確認する → 誰でも検証できる

これにより、

□ 誰が作ったかが分かる(認証)
□ 改ざんされていないことが分かる(完全性)
□ 「作っていない」と言えない(否認防止)  ← HMAC には無い性質

JWT の署名(認証と認可の実装)、証明書、パッケージの署名が、これです。

HMAC と署名の使い分け
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` と書かれていました。何を指摘しますか。

実務の落とし穴まとめ

  1. 暗号を自作する — 動いているように見えて破れる
  2. ハッシュを暗号化と呼ぶ — 戻せるかどうかが根本的に違う
  3. パスワードを SHA-256 で保存 — 専用の関数を使う(認証と認可の実装)
  4. Math.random() でトークン生成 — 予測される
  5. 署名の比較に == — タイミング攻撃
  6. 鍵をコードに書く / git にコミット — 即座にクロールされる
  7. ローテーションできない設計 — 漏洩時に切り替えられない
  8. Base64 を暗号と勘違い — 誰でも戻せる
  9. 古いアルゴリズムを使う — MD5 / SHA-1 / DES は使わない

まとめ

  • 暗号は機密性・完全性・認証・否認防止のどれを守るかで手段が変わる
  • ハッシュは戻せない(改ざん検知・同一性判定)。 パスワードには bcrypt / argon2
  • HMAC は鍵を共有する2者間、署名は第三者にも検証できる
  • 公開鍵暗号は鍵配送を解決するために生まれた。TLS は両方を組み合わせる
  • 証明書は「公開鍵が本物か」を CA が保証する仕組み
  • トークンには暗号論的乱数を使う
  • 実務の本体は鍵の管理。コードに書かず、権限を絞り、 ローテーションできる設計にする
  • 署名の比較は定数時間比較
  • 「暗号を設計している」と感じたら、止まって相談する

公式ドキュメント・参考資料

CRYPTREC は「どのアルゴリズムを使ってよいか」の日本での基準です。 迷ったらここを見ると、選択の根拠になります。

次の章では、これらの防御をいつ・どう設計に組み込むかを扱います。

読み終わったら記録しておくと、目次で進み具合が分かります。