1-3. HTTPSとTLS - 通信路を盗聴・改ざんから守る

所要時間: 30-50分 ゴール: HTTPSの仕組み(TLSハンドシェイク・証明書チェーン・CA)を概念レベルで説明できる。MITM、HSTS、Let’s Encryptを理解する コミット内容: 自サービスのドメインを openssl s_client で覗いて証明書情報を ~/learn/security/day03_tls/ に保存


前章とのつながり

このレッスンの位置づけ

1-2 パスワード保存bcrypt でパスワードハッシュを完璧に守った。でも、ログイン時にパスワードがネットワークを平文で流れていたら? 同じカフェの隣の人が pcap で全部見ている。

サーバー側の守りと通信路の守りは 両輪。片方欠ければ他方も無意味。

実害シーン: 通信路を守らないとどうなるか

  • Firesheep (2010): ブラウザ拡張1つで、同じカフェの全員の Facebook セッションが奪える。これが HTTPS 必須化の起爆剤
  • 某航空会社の機内Wi-Fi (継続事案): HTTP のページに広告 JS が挿入され、認証情報窃取の足がかりにされる
  • 公衆Wi-Fi MITM: 「Free Airport WiFi」を名乗る偽APが、ログイン情報を全部キャプチャ。被害は今も報告されている

「うちは社内ツールだから HTTP でいい」は 退職者・委託先・侵入された LAN を考慮していない発想。社内こそ HTTPS


大前提: なぜ HTTPS が必須なのか

ブラウザで http:// のサイトを開くと「保護されていない通信」と警告が出る。あれは脅し文句ではない:

  • 盗聴: 同じWi-Fi(カフェ、空港)にいる人があなたの通信を全部読める
  • 改ざん: ISP・公衆Wi-Fi・国家ファイアウォールが通信内容を書き換えられる(広告挿入、悪意あるJS埋め込み)
  • なりすまし: 「これは本物の example.com ですよ」を保証する仕組みが無い

HTTPS(=HTTP + TLS)はこの3つを同時に解決する。

実際に起きた事件:

  • Firesheep (2010): カフェのWi-Fiで他人のFacebookセッションが見え放題だった事件(当時のFacebookはHTTPS必須でなかった)。これでHTTPS化の流れが加速
  • 中国の Great Cannon (2015): ISPレベルで広告タグを書き換え、GitHubへのDDoS攻撃に流用された。HTTPS化していたサービスは影響を受けなかった
  • 公衆Wi-Fi での個人情報窃取: 「Free Wi-Fi」を名乗る悪意あるアクセスポイントが今も多数存在

2026年現在、HTTPS は「やったほうがいい」ではなく「やらなければ違法・恥」のレベル。Let’s Encrypt のおかげで実質無料・自動化可能。やらない理由がない。


セッション①: HTTPS と TLS の基礎(25-30分)

0. 録画&作業ディレクトリ

mkdir -p ~/log ~/learn/security/day03_tls
cd ~/learn/security/day03_tls
script ~/log/security_day03.log

1. HTTPS とは何か

HTTP(生のWeb通信)
  ↓
HTTP over TLS = HTTPS
  ↓
通信内容が暗号化される

HTTPS の構造

HTTPS = HTTP をそのまま TLS という暗号化トンネルの中に流すだけ。

  • HTTP 自体は変わらない(同じ GET / POST / ヘッダー)
  • 下のレイヤーが暗号化される
  • サーバー側もアプリは HTTP しか喋らず、リバースプロキシ(nginx等)が TLS を終端することが多い

TLS(Transport Layer Security): 「SSL」と呼ばれていた古い名前を引き継いだ後継規格。

  • SSL 1.0/2.0/3.0 → TLS 1.0 → TLS 1.1 → TLS 1.2 → TLS 1.3(2018-)
  • 2026年現在はTLS 1.2 と 1.3 のみが現役。TLS 1.0/1.1 はすでに非推奨

2. TLS が解決する3つの脅威

TLSが保証する3つの性質

性質解決する脅威技術的手段
機密性(Confidentiality)盗聴対称暗号化(AES等)
完全性(Integrity)改ざんMAC / AEAD
認証(Authentication)なりすまし証明書 + 公開鍵暗号

この3点セットを CIA と呼ぶ(セキュリティの基本概念)。情報セキュリティの定義そのもの。

3. TLSハンドシェイクの概要

[クライアント]                          [サーバー]
   │                                       │
   │  ── ClientHello ──→                   │  「対応する暗号スイート教えて」
   │       (TLS版本、暗号候補、乱数)       │
   │                                       │
   │  ←── ServerHello ─────                │  「これ使うね、証明書はこれ」
   │       (選んだ暗号、証明書、乱数)      │
   │                                       │
   │  証明書を検証                          │
   │  (CAの署名で本物か確認)               │
   │                                       │
   │  ── 鍵交換 ────────→                  │  公開鍵暗号で対称鍵を共有
   │  ←─ 鍵交換 ────────                   │  (ECDHE で前方秘匿性)
   │                                       │
   │  対称鍵で暗号化通信開始 ←─────────→  │

ハンドシェイクの本質

「公開鍵暗号で 対称鍵を安全に共有 → その後は対称暗号で高速通信」

なぜ全部を公開鍵でやらないのか:

  • 公開鍵暗号(RSA、ECDSA)は 非常に遅い(対称暗号の数百倍)
  • 大きなデータ転送に向かない

対策:

  • 「鍵共有だけ公開鍵」「実データは対称鍵」のハイブリッド
  • 対称鍵は セッションごとに生成・破棄(一度漏れても他セッションは安全 = 前方秘匿性 / Forward Secrecy

TLS 1.3 で何が変わったか

  • ハンドシェイクが 1往復(1-RTT) に短縮(1.2では2往復)
  • 0-RTT 再開モード(過去に通信したサーバーへの再接続が高速化)
  • 古い暗号アルゴリズム(RC4、3DES、SHA-1)を全廃
  • デフォルトで Forward Secrecy 必須

2026年新規構築なら TLS 1.3 だけ有効化するのが理想。クライアント互換性で 1.2 も残すのが現実解。

4. 公開鍵暗号の超ざっくり理解

公開鍵暗号の魔法

鍵を2つ作る:「公開鍵」と「秘密鍵」。

  • 公開鍵で暗号化したものは、対応する 秘密鍵でしか復号できない
  • 秘密鍵で署名したものは、対応する 公開鍵で検証できる

これにより:

  • 暗号化: 「相手の公開鍵で暗号化」→ 相手しか開けない(鍵を事前共有する必要がない)
  • 署名: 「自分の秘密鍵で署名」→ 受け取った人は公開鍵で検証 → 「確かにあなたが書いた」と判明

アルゴリズム例: RSA(古典)、ECDSA / Ed25519(楕円曲線、現代的)

TLSではこれを「対称鍵の共有」と「サーバー身元の証明」に使う

5. 証明書チェーンと認証局(CA)

ルート証明書(DigiCert Root CA)         ← OSやブラウザに最初から入っている
        │ (秘密鍵で署名)
        ↓
中間証明書(DigiCert Intermediate)
        │ (秘密鍵で署名)
        ↓
サーバー証明書(example.com)            ← サーバーが提示する

証明書チェーンとは

サーバーが「私は example.com です」と自称するだけでは信用できない。第三者の 認証局(CA: Certificate Authority) に証明書を発行してもらう。

CAは厳しい審査の末、「このドメインは確かにこの組織のもの」と署名してくれる。

ブラウザは:

  1. サーバー証明書を取得
  2. その証明書が中間CAで署名されている → 中間CA証明書を確認
  3. 中間CAがルートCAで署名 → ルートCAは OS/ブラウザに事前インストールされている ので信頼OK

このチェーンが切れていたり、ルートCAが信頼リストに無いと「証明書エラー」になる。

認証局(CA)の例

  • Let’s Encrypt: 無料・自動。世界中の半分以上のWebサイトが利用
  • DigiCert / GlobalSign / Sectigo: 有償の老舗。EV証明書(緑バー)など
  • Cloudflare: CDN経由で証明書も提供

CAは「ドメインの本人確認」だけする(DV: Domain Validation)。法人の実在まで確認するEV(Extended Validation)は高い。現代では DV で十分という風潮。

6. Let’s Encrypt と ACME プロトコル

Let's Encrypt の革命

2015年開始、無料、自動更新可能なCA。事実上、Webから「証明書取得の障壁」を消した

仕組み:

  1. ACME(Automated Certificate Management Environment)プロトコルでCAと通信
  2. CA「あなたが本当に example.com の所有者なら、http://example.com/.well-known/acme-challenge/xxx に特定の文字列を置いて」(HTTP-01 チャレンジ)
  3. クライアントが指定された場所にファイル配置
  4. CAがアクセスして確認
  5. OKなら証明書発行

これを 証明書発行のたびに自動実行。クライアントツール「certbot」が定番。

ユースケース: 個人ブログから大企業のサービスまで。ほぼ全Webサービスのデフォルト選択肢 落とし穴:

  • 有効期限は 90日 と短い。自動更新スクリプト必須(cronで動かす)
  • レート制限あり(同一ドメインで週20件など)
  • ワイルドカード証明書はDNS-01 チャレンジ(DNSレコードで認証)が必要

7. 実際に証明書を覗いてみる

# Google の証明書を取得して表示
openssl s_client -connect google.com:443 -servername google.com </dev/null 2>/dev/null \
  | openssl x509 -noout -text
 
# 重要情報だけ取り出す
openssl s_client -connect google.com:443 -servername google.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates
 
# ファイルに保存して観察
openssl s_client -connect google.com:443 -servername google.com </dev/null 2>/dev/null \
  | openssl x509 > ~/learn/security/day03_tls/google.crt

観察ポイント

  • Subject: 誰のための証明書か(CN = Common Name、SAN = Subject Alternative Name に複数ドメイン)
  • Issuer: 誰が署名したか(CA名)
  • Not Before / Not After: 有効期間
  • Signature Algorithm: 署名アルゴリズム(sha256WithRSA、ecdsa-with-SHA256 など)
  • Public Key: 公開鍵情報

ブラウザでも確認できる

アドレスバーの鍵マーク → 「証明書」で同じ情報が見られる。社内サイトや本番デバッグで重宝。


セッション②: 攻撃と対策、開発時の落とし穴(25-30分)

8. MITM(Man-in-the-Middle / 中間者)攻撃

[本物のクライアント] ←→ [攻撃者] ←→ [本物のサーバー]
                       ↑ 通信を中継しながら盗聴・改ざん

MITM 攻撃のシナリオ

  1. 公衆 Wi-Fi で偽アクセスポイントを立てる
  2. ユーザーがそこに繋ぐ
  3. 攻撃者がユーザーとサーバー両方と別々に通信を確立
  4. 通過する通信を読み放題、書き換え放題

HTTPSが防ぐ仕組み:

  • サーバー証明書をCAで検証 → 攻撃者は本物の証明書を持っていないので、ブラウザがエラーを出す
  • 暗号化されているので、中身が見えても読めない

HTTPSが破られるパターン:

  • ユーザーが証明書エラーを無視して「進む」を押す
  • 攻撃者が偽CAをユーザーのPCに事前インストール(社内ネット監視用ProxyとかMDM経由とか)
  • サーバー側で HTTP のままレスポンスしてしまうエンドポイントがある(HSTSで防ぐ)

9. HSTS(HTTP Strict Transport Security)

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

HSTS の本質

サーバーが「このドメインには次から1年間、HTTPSでしかアクセスするな」とブラウザに命令するHTTPヘッダー。

何を防ぐか:

  • SSL Strip 攻撃: ユーザーが example.com と打つ → ブラウザは最初HTTPでアクセス → 攻撃者がここをHTTPで奪う、というシナリオを潰す
  • ユーザーが手で http:// と打っても、ブラウザが内部的に https:// に書き換えてアクセス

オプション:

  • max-age: 何秒間この設定を覚えておくか(1年 = 31536000 が標準)
  • includeSubDomains: サブドメインも含めて強制
  • preload: HSTS Preload List に登録されると、初回アクセスから HTTPS 強制 される

落とし穴: 一度有効にすると max-age が切れるまで戻せない。プレリロードに登録すると、削除には数ヶ月かかる。本番投入前に開発環境で十分テスト

10. Certificate Pinning(証明書ピンニング)

Pinning の本質

アプリ(特にモバイル)が「このサーバーが提示する証明書はこれと一致してないと拒否」というロジックを アプリ内に焼き込む

何を防ぐか:

  • 不正なCAが攻撃者用に発行した偽証明書(CA自体が攻撃された場合)
  • MDM経由でユーザー端末に偽CAを仕込まれている場合

ユースケース: 銀行アプリ、決済アプリ、メッセンジャー(Signal等) 落とし穴:

  • 証明書を更新するたびにアプリ更新が必要 → 運用負荷大
  • ミスると アプリ全停止(証明書更新したらユーザー全員が繋がらない)
  • 一般的なWebサービスでは過剰、モバイルアプリでも採用は慎重に
  • 2017年頃のPKP(HTTP Public Key Pinning)はブラウザではすでに非推奨

11. 開発時の落とし穴

自己署名証明書(self-signed cert)

# 自己署名証明書を作る(開発用)
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes \
  -subj "/CN=localhost"

自己署名証明書の罠

  • CAが署名していない → ブラウザは「危険」と警告
  • 開発時の許容方法:
    • ブラウザで「詳細設定 → 進む」(自分のローカルでだけ)
    • 開発マシンの信頼ストアに証明書を追加
    • mkcert ツールを使うとローカル開発用CAを楽に作れる(推奨)
  • 本番に絶対持ち込まない: お客様に証明書警告を出すサービスは終わってる
# mkcert を使ったほうが楽
brew install mkcert
mkcert -install
mkcert localhost 127.0.0.1
# → localhost-key.pem / localhost.pem が生成され、ローカルで信頼される

localhost と HTTPS

localhost は実は特別扱い

  • ブラウザは http://localhost を「安全な文脈(secure context)」とみなす
  • これは Service Worker、WebCrypto、HTTPS 必須の API を localhost でも使えるようにするため
  • 127.0.0.1 は別扱い だったが、現代ブラウザでは localhost と同等に扱われる

ただし「同じネットワーク上の他端末からアクセスする」場合(モバイル実機テスト)は localhost ではないので、自己署名 or mkcert が必要

混在コンテンツ(Mixed Content)

<!-- HTTPS ページの中で HTTP のリソースを読み込む -->
<script src="http://example.com/old.js"></script>
<!-- ↑ これでブラウザが「混在コンテンツ」と警告 -->

Mixed Content の問題

HTTPSページ内で HTTP リソースを読み込むと、その HTTP リソースは MITM 可能 = ページ全体の安全が破綻する。

ブラウザの対応:

  • Active content(script、iframe、CSS): ブロックされる
  • Passive content(img、video): 警告のみ(または自動 https へ昇格)

対策: 全リソースを HTTPS で読み込む。//cdn.example.com/lib.js のような protocol-relative URL はもう使わず、明示的に https:// を書く

ローカル開発でHTTPSにする方法

# 1. mkcert で証明書生成
mkcert localhost
 
# 2. Node.js / Goのサーバーで起動
# Node:
# https.createServer({key, cert}, app).listen(443)
 
# 3. Caddy を使うとさらに楽(ローカルもLet's Encryptも自動)
brew install caddy
echo "localhost {
  reverse_proxy localhost:3000
}" > Caddyfile
caddy run

Caddy で開発もHTTPS

Caddy は ローカルなら mkcert 自動、本番なら Let’s Encrypt 自動 で HTTPS化してくれる。設定ファイルが極小で済む。最近の推し。

12. 暗号スイートと現代の選択

暗号スイートとは

TLSハンドシェイクで「これ使って通信しましょう」と合意する アルゴリズムの組合せ:

TLS_AES_256_GCM_SHA384
    │       │   │
    │       │   └─ メッセージ認証用ハッシュ
    │       └───── 対称暗号モード(GCMが現代の主流)
    └─────────── 対称暗号アルゴリズム

2026年現在の推奨:

  • TLS 1.3 がサポートするスイートはすべて安全(古いのを切り捨てた設計)
  • TLS 1.2 では ECDHE 系(前方秘匿性あり)+ AES-GCM + SHA-256/384 を選ぶ
  • RC4、3DES、CBC モード、SHA-1 などは無効化

設定支援: Mozilla の SSL Configuration Generator が便利。nginx/Apache の設定をクリックで生成

13. nginx での TLS 設定の最小例

server {
    listen 443 ssl http2;
    server_name example.com;
 
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
 
    # 現代的な設定(Mozilla Intermediate)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;
 
    # HSTS(HTTPS 必須化)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
 
    location / {
        proxy_pass http://localhost:3000;
    }
}
 
# HTTP は HTTPS にリダイレクト
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

このnginx設定で起きていること

  • TLS 1.2/1.3 のみ受け付け
  • HSTS で「次回からHTTPS強制」をブラウザに指示
  • HTTPで来た接続を301でHTTPSに転送
  • 内部の Node/Go アプリは普通の HTTP で動かす(TLS終端は nginx 担当)

練習課題

# 1. 自分が普段使うサービスの証明書を3つ覗く
for domain in google.com github.com github.io; do
  echo "=== $domain ==="
  openssl s_client -connect $domain:443 -servername $domain </dev/null 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates
  echo ""
done > ~/learn/security/day03_tls/certs.txt
 
# 2. ローカル開発用に mkcert を入れる
brew install mkcert
mkcert -install
mkcert localhost
 
# 3. Goで簡単なHTTPSサーバーを立ててブラウザで開く
cat > server.go << 'EOF'
package main
 
import (
	"fmt"
	"log"
	"net/http"
)
 
func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello over HTTPS!")
	})
	log.Println("Listening on https://localhost:8443")
	log.Fatal(http.ListenAndServeTLS(":8443", "localhost.pem", "localhost-key.pem", nil))
}
EOF
 
go run server.go
# 別ターミナルで: curl https://localhost:8443

締め: 振り返り(10分)

1. セッション録画を終了

exit

2. 今日の発見

- TLSハンドシェイクで「対称鍵を共有する」が腑に落ちたか:
- 自分のサービスは HSTS を入れているか:
- 自己署名証明書を本番で使っている場面に出会ったら何と言うか:
- 明日やりたいこと:

チェックリスト

  • HTTPSの内部構造(HTTP + TLS)が言える
  • TLSが保証する3つの性質(機密性・完全性・認証)を言える
  • TLSハンドシェイクで「公開鍵→対称鍵に乗り換える」流れが言える
  • 証明書チェーン(ルート → 中間 → サーバー)を絵で書ける
  • Let’s Encrypt と ACME を1分で説明できる
  • HSTS が何を防ぐか言える
  • openssl s_client でサイトの証明書を覗いた
  • mkcert でローカルHTTPS環境を作った

詰まった時のチートシート

やりたいことコマンド/設定
サイトの証明書を見るopenssl s_client -connect host:443 -servername host
証明書の有効期限を確認`…
証明書をファイル化`…
ローカル用HTTPSmkcert localhost
HSTSヘッダーStrict-Transport-Security: max-age=31536000; includeSubDomains
HTTP→HTTPSリダイレクト(nginx)return 301 https://$host$request_uri;
Let’s Encrypt取得certbot --nginx -d example.com
TLS 1.3のみ有効化(nginx)ssl_protocols TLSv1.3;
HTTPS版図解HTTP の中身が暗号化される、それだけ

「実務OK」基準

  • HTTPS必須を当然と思える: 「うちは社内だからHTTP」とか言わない
  • openssl s_client でサーバーの証明書を即座に確認できる
  • mkcert などでローカルHTTPS環境を構築できる
  • 混在コンテンツ警告に遭遇したら原因をすぐ突き止められる
  • HSTSのリスクと利点を両方説明できる

メタ認知: 自分の理解度を診断する

30秒セルフテスト

  1. TLS ハンドシェイクで「鍵交換」と「認証」はそれぞれ何で行われる?
  2. CA・中間証明書・サーバー証明書の チェーン が必要な理由を1文で
  3. HSTS が「設定したら戻せない」と言われる理由は?
  4. Let’s Encrypt の証明書有効期限は何日?
  5. openssl s_client -connect example.com:443 で何が分かる?

詰まったら → セッション①のハンドシェイク図を再描画してみる。手で描くと記憶定着率が跳ねる。

次のレッスン

1-4 OWASP Top 10 でWebアプリ脆弱性の代表選手10種を概観する。HTTPSで通信路は守れても、アプリ自体に穴があれば意味がない。攻撃の世界を網羅的に覗く回。

間隔反復ポイント

TLS の細部は 半年も触らないと忘れる。3ヶ月後にこのページの「詰まった時のチートシート」を眺め直すと、certbot コマンドや HSTS 設定が即座に出てくる状態を維持できる。