セキュリティは「重要だと誰もが認めるが、自信を持って理解していると言える人は少ない」典型的なテーマです。原因のひとつは語彙の威圧感 — 暗号、ソルト、HMAC、TOTP、非対称鍵 — もうひとつは、間違いの代償が事故が起きるまで見えないことです。
このガイドでは語彙の壁を取り払い、Web開発者として日々下す具体的な意思決定にフォーカスします。パスワードをどう保存するか、データにどう署名するか、ユーザーをどう認証するか、トークンをどう生成・検証するか。各概念にはブラウザで動く検証ツールが対応しており、理解の確認と実装の検証に使えます。
これで暗号学者になれるわけではありません。しかし、Web開発で最も重要なセキュリティプリミティブの実用的なメンタルモデルは身につきます。
ハッシュ:すべての基盤
ハッシュ関数は任意長の入力を固定長の出力に変換します。同じ入力からは常に同じ出力、異なる入力からは(ほぼ確実に)異なる出力が得られます。そして決定的に重要なのは、ハッシュから元の入力を逆算できないことです。
この一方向性こそがハッシュをセキュリティに有用にしています。
ハッシュの使い道:
- パスワード保存(平文では絶対に保存しない — ハッシュを保存)
- ファイル整合性検証(ダウンロードしたファイルのハッシュを既知の正しいハッシュと比較)
- 決定的な識別子の生成(構造化入力をハッシュ化して安定したIDを得る)
- データ改ざん検出(受信データのハッシュが期待値と一致すれば未改ざん)
主なハッシュアルゴリズム:
| アルゴリズム | 出力長 | 現在も使える? |
|---|---|---|
| MD5 | 128 bit | 不可 — 衝突発見済み、セキュリティ用途では使用禁止 |
| SHA-1 | 160 bit | 不可 — 暗号用途では非推奨 |
| SHA-256 | 256 bit | 可 — 大半の用途で堅実な選択 |
| SHA-512 | 512 bit | 可 — 出力が長く、総当たりがわずかに困難 |
| bcrypt | 可変 | 可 — パスワード専用に設計 |
ハッシュジェネレーターでハッシュを計算してみてください。同じ入力をMD5とSHA-256に入れて出力を比較し、入力を1文字変えるだけでハッシュが完全に変わる様子を確認します(これは雪崩効果と呼ばれ、意図的な設計です)。
**パスワードハッシュに関する重要な注意:**パスワードを生のSHA-256やMD5でハッシュ化してはいけません。これらのアルゴリズムは高速で、それはパスワード用途では最も望ましくない性質です — 高速なハッシュは攻撃者がコモディティハードウェアで毎秒数十億パスワードを試せることを意味します。パスワードにはbcrypt、scrypt、Argon2を使ってください。意図的に低速で計算コストが高くなるよう設計されています。
ソルト:パスワードをハッシュ化するだけでは不十分な理由
同じパスワードを同じアルゴリズムでハッシュ化すれば、結果は常に同じです。つまり攻撃者がハッシュ化されたパスワードDBを盗めば、事前計算テーブル(レインボーテーブル)でよくあるパスワードを逆引きできます。password123のSHA-256値は常に同じなので、その値がDBに現れれば、ユーザーごとの計算なしでパスワードが判明します。
ソルトはパスワードに加えてからハッシュ化されるランダム値です。各ユーザーは固有のソルトを持ち、ハッシュと一緒に保存されます。これで同じパスワードを使う2人のユーザーでも異なるハッシュになり、レインボーテーブルは無力化されます — ソルト値ごとにテーブルを作り直す必要があるためです。
具体的には:
# ソルトなし — 同じパスワード、同じハッシュ
sha256("password123") → a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3
# ソルトあり — 同じパスワード、ユーザーごとに異なるハッシュ
sha256("password123" + "user_alice_salt_xyz") → 別のハッシュ
sha256("password123" + "user_bob_salt_abc") → さらに別のハッシュ
現代のパスワードハッシュライブラリ(bcrypt、Argon2)はソルト生成を自動で行います。手動管理は不要ですが、なぜ必要なのかを理解しておくとライブラリが正しく機能しているか判断できます。
HMAC:データが改変されていないことを署名で証明する
ハッシュ単体は、データが特定の出力にマップされることしか教えてくれません。誰が作ったのか、信頼できる相手から来たのかは分かりません。
**HMAC(Hash-based Message Authentication Code)**はハッシュと秘密鍵を組み合わせます。出力される署名は2つを同時に証明します:データの内容(通常のハッシュと同様)と、作成者が秘密鍵を持っていたこと。
Web APIで広く使われています:
- Webhook署名(Stripe、GitHubなどはHMACでwebhookペイロードに署名し、送信元の検証を可能にする)
- APIリクエスト署名(AWS Signature Version 4はHMACでAPIリクエストに署名)
- Cookieの改ざん防止(HMACでcookie内容に署名し、リクエストごとに検証)
HMAC ジェネレーターで試してみてください。秘密鍵でメッセージのHMACを生成し、メッセージを1文字変えて再生成すると、HMACは完全に変わります。秘密鍵を知らずに元のHMACを再現できないことも確認できます。
Webhook検証の実装例:
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
# タイミング攻撃を防ぐため hmac.compare_digest を使用
return hmac.compare_digest(expected, signature)
==ではなくhmac.compare_digestを使う点に注目。タイミング攻撃は実在の脅威です:素朴な文字列比較は不一致を見つけた瞬間に早期リターンし、何文字一致したかの情報を漏らします。compare_digestは不一致がどこにあろうと定数時間で動作します。
JWT:コンパクトなクレームトークン
JSON Web Tokenは現代Webアプリで認証クレームを運ぶ主流フォーマットです。JWTはピリオドで区切られた3つのbase64urlエンコードされたJSONオブジェクトで構成されます:ヘッダー、ペイロード、署名。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
ヘッダーにはアルゴリズム、ペイロードにはクレーム、署名(秘密鍵または秘密鍵ペアの秘密鍵で計算)が改ざんを防ぎます。
JWT デコーダーで任意のJWTの中身を確認できます。デコーダーは秘密鍵なしでヘッダーとペイロードを表示します。
JWTの重要なセキュリティルール:
**1. 必ず署名を検証する。**デコード ≠ 検証。どんなライブラリでも秘密鍵なしにJWTをデコードできます — 検証されていないJWTのクレームは真正性について何も語りません。バックエンドはリクエストごとに、期待する秘密鍵または公開鍵で署名を検証する必要があります。
**2. exp(有効期限)クレームを確認する。**期限切れのJWTは署名が有効でも拒否すべきです。「ログアウト後もユーザーが認証されたままなのはなぜか?」というバグの多くは、検証パスがexpを実際に強制していないことが原因です。
**3. alg: noneに注意する。**一部のJWTライブラリは過去にヘッダーが"alg": "none"のトークンを受け入れ、署名検証を完全にバイパスしていました。ライブラリがnoneを明示的に拒否することを確認してください。
**4. 重要なアプリではJWTをlocalStorageに保存しない。**localStorageはJavaScriptからアクセス可能で、XSS脆弱性が1つあれば全トークンが盗まれます。価値の高い認証トークンにはHttpOnly cookieが適切です — JavaScriptから読めません。
**5. ペイロードは最小限に保つ。**JWTペイロードはbase64エンコードされているだけで暗号化されていません。トークンを持つ人は誰でもクレームを読めます。秘密、パスワード、機微なPIIをペイロードに入れないでください。
TOTP:時刻ベースのワンタイムパスワード(認証アプリの仕組み)
二要素認証はユーザーアカウントを持つあらゆるアプリの基本的なセキュリティ要件です。内部の仕組みを理解しておくと正しく実装でき、壊れたときのデバッグにも役立ちます。
**TOTP(Time-Based One-Time Password、RFC 6238)**は次のように動作します:
- セットアップ時、サーバーが秘密(通常20バイトのランダム値、base32エンコード)を生成
- その秘密がQRコード経由でユーザーの認証アプリに共有される
- これでサーバーと認証アプリは同じ秘密を共有
- コード生成時、両側が
HMAC-SHA1(secret, floor(current_time / 30))を計算 - 結果を6桁の数値に切り詰め
- 同じ秘密と同じ現在の30秒ウィンドウを使うので、両側で同じ6桁コードが得られる
QRコードは秘密とメタデータをURIエンコードしたものです:otpauth://totp/Issuer:[email protected]?secret=BASE32SECRET&issuer=Issuer&algorithm=SHA1&digits=6&period=30
TOTP ジェネレーターでTOTP生成を試してください。秘密を生成し、コードが30秒ごとに変わる様子を観察します — Google AuthenticatorやAuthyの内部で起きていることそのものです。
TOTP実装でよくある間違い:
- クライアントとサーバー間の時刻ずれを許容する小さなウィンドウ(±1周期)を設けない
- 同じ30秒ウィンドウ内でのコード再利用を防がない
- TOTPの秘密を平文で保存する(暗号化するか秘密管理サービスに保管)
- 非常に長い周期(30秒ではなく300秒)を使う — 利便性は上がるが攻撃ウィンドウは大きく広がる
非対称暗号:署名と暗号化のためのRSA鍵
これまでのプリミティブはすべて対称暗号 — 同じ秘密で生成と検証を行います。非対称暗号は数学的に結びついた2つの鍵を使います:公開鍵と秘密鍵。一方で暗号化したものはもう一方でしか復号できず、一方で署名したものはもう一方で検証できます。
RSAは最も広く使われる非対称アルゴリズムです。主な用途:
- TLS証明書(サーバーが提示する証明書には公開鍵が含まれ、ブラウザは秘密鍵を知らずに暗号化接続を確立)
- RS256によるJWT(サーバーが秘密鍵でJWTに署名し、クライアントは公開鍵で検証 — 署名秘密を信頼できる相手に渡す必要がない)
- SSH鍵(
~/.ssh/id_rsa.pubは公開鍵;秘密鍵は手元のマシンに残る;サーバーは公開鍵を保管し、秘密鍵がローカルを離れることなく認証可能) - コード署名(ソフトウェア発行者が秘密鍵でリリースに署名し、ユーザーが公開鍵で検証)
RSA 鍵ペアジェネレーターでRSA鍵ペアを生成してください。公開鍵が秘密鍵から派生していることに注目 — 独立ではなく、同じ数学的オブジェクトの両面です。
秘密鍵の取り扱い — 譲れないルール:
- 秘密鍵をバージョン管理にコミットしない。一時的にも、プライベートリポジトリでも。
- 秘密鍵を平文チャネル(メール、Slackなど)で送らない
- 秘密鍵は秘密管理サービス(AWS Secrets Manager、HashiCorp Vault、Google Secret Manager)に保管
- アクセス権を持っていた人が組織を離れたら鍵をローテーション
公開鍵暗号を強力にする非対称性は、同時に容赦のなさの源でもあります。公開鍵は自由に共有できます — Webサイトに掲載、APIレスポンスに含める、クライアントアプリにハードコードしてもよい。しかし秘密鍵が漏れた瞬間、それに依存していたすべてのセキュリティ保証が無効になります。
パスワード生成:「強い」の本当の意味
ほとんどのセキュリティシステムの最弱リンクはユーザーが選んだパスワードです。ユーザーに選ばせると、相当数がpassword123、ペットの名前、生年を選びます。生成したり複雑度要件を強制すると結果は通常マシになりますが、必ずしも望ましいとは限りません。
何がパスワードを強くするのか?
**長さが最重要因子。**95文字のアルファベットから選ぶ8文字パスワードは約53ビットのエントロピー、同じアルファベットの16文字パスワードは約105ビット。攻撃者のコストはエントロピー1ビットごとに2倍になります。長さは最も効果的なレバーです。
**ランダム性は複雑度より重要。**ランダムな小文字12文字パスワードは予測可能な「複雑」パスワードP@ssw0rd!1より強いです。複雑度要件はパスワードを覚えにくくする一方で推測しにくくはせず、人間が予測可能なパターンを作るためです。
資格情報の生成(一時パスワード、APIキー、サービスアカウント認証情報、リカバリコード)にはパスワードジェネレーターを使ってください。ブラウザベースのツールによる暗号学的にランダムな生成は信頼でき、生成値をサードパーティサーバーに晒しません。
ユーザー作成パスワードには強力なパスワードハッシュ関数(コストファクター12以上のbcrypt、またはArgon2id)を使い、漏洩パスワードDBで照合してください(Have I Been PwnedのAPIはプライバシー保護のレンジクエリに対応)。
まとめ:セキュアな認証チェックリスト
ユーザー認証を持つWebアプリでは以下を確認してください:
- パスワードはユーザーごとのソルト付きでbcrypt/Argon2idハッシュとして保存(平文や生のSHA-256は不可)
- JWTはリクエストごとに検証(署名 + 有効期限 + issuer + audience)
- Webhookペイロードは処理前にHMACで検証
- TOTPまたは他の2FAが利用可能(管理者アカウントには必須)
- RSAやECDSAの秘密鍵は秘密管理サービスに保管、ソースコードには絶対に置かない
- HTTPSを全面強制(ミックスドコンテンツなし、HSTSヘッダー設定済み)
- 機微なcookieはHttpOnlyとSecureフラグを設定
- パスワードリセットトークンは使い切りで短命(≤15分)
- 認証エンドポイントにレート制限
セキュリティは最後に追加する機能ではありません。設計と実装の各段階で下す決定の集合です。プリミティブ — ハッシュ、HMAC、JWT、TOTP、RSA — はよく理解され、よくドキュメント化され、主要言語の標準ライブラリで利用可能です。
Webアプリのセキュリティ脆弱性の主因は暗号知識の不足ではありません。正しいプリミティブの誤用です:低速ハッシュが必要な場所で高速ハッシュを使う、JWTの署名検証を忘れる、アクセスしやすすぎる環境に秘密を保管する。
プリミティブを正しく適用できる深さで理解することが実用的なゴールです。上記のツールはその理解を構築・検証するのに役立ちます。使ってください — 何かおかしく見えたら、フォーマットし、デコードし、実際の値を読んでから判断してください。
セキュリティはいつも細部に宿ります。