Step 02 — TLS (サーバー認証のみ)
ゴール
tls.Listen/tls.Dialを使って TLS で暗号化された TCP を成立させる。- サーバーが提示する証明書を、クライアントが「信頼する CA」で検証するという流れを掴む。
tls.Configの主要フィールドCertificates/RootCAs/ServerNameの役割を説明できるようにする。
このステップでは クライアントは身元を提示しない。Step03 (mTLS) でそれを足します。
前提
証明書が必要です。リポジトリのルートで一度だけ実行してください。
make certs
# あるいは
./scripts/gen-certs.shtutorial/step02-tls/certs/ 直下に ca.crt, server.crt, server.key が並べば OK です。
ファイル
| ファイル | 役割 |
|---|---|
server/main.go | :9443 で TLS listen。server.crt を提示 |
client/main.go | localhost:9443 に TLS で接続。ca.crt でサーバーを検証 |
certs/ | gen-certs.sh が配置 (.gitignore で除外) |
動かす
ターミナル A
go run ./tutorial/step02-tls/server
# => step02 TLS server listening on :9443 (server auth only)ターミナル B
go run ./tutorial/step02-tls/client
# => connected to localhost:9443 over TLS (server CN=localhost)
hello
echo: helloハンドシェイクで何が起きているか
- クライアントが
ClientHelloを送る (使える暗号アルゴリズム一覧、SNI=localhostなど)。 - サーバーが
ServerHelloと 自分の証明書チェーン を返す。 - クライアントは:
- 証明書のチェーンが
RootCAsのどれかに辿り着くか検証 - 証明書の SAN に
ServerName(=localhost) が含まれるか検証 - 証明書の有効期限内か、署名が壊れていないかを検証
- 証明書のチェーンが
- 鍵交換 → 共通鍵を作って暗号化セッション開始。
- 以降のアプリケーションデータは AES などの共通鍵暗号で暗号化される。
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: TCP 3-way ハンドシェイク
C->>S: SYN (seq=x)
S->>C: SYN+ACK (seq=y, ack=x+1)
C->>S: ACK (ack=y+1)
Note over C,S: ESTABLISHED
Note over C,S: TLS ハンドシェイク
C->>S: ClientHello
S->>C: ServerHello, Certificate
C->>S: (鍵交換)
Note over C,S: 暗号化セッション確立
ここまでを通ると、Step01 にあった「盗聴」「改ざん」「サーバーのなりすまし」が解決されます。
観察ポイント (実験してみよう)
1. ServerName を変えると失敗する
client/main.go の serverName を "example.com" に書き換えて実行すると、ハンドシェイクが失敗します。証明書の SAN は localhost / 127.0.0.1 だけで、example.com ではないからです。
Go 1.15 以降、証明書の CommonName ベースの検証は廃止されています。
subjectAltName (SAN)のみが見られます。gen-certs.shで SAN を明示しているのはこのためです。
2. CA を信頼しないとどうなるか
クライアント側で RootCAs を渡さない (pool の設定をコメントアウト) と、x509: certificate signed by unknown authority で失敗します。OS の信頼ストアにも我々のオレオレ CA は入っていないからです。
3. Step01 のクライアントは喋れない(平文 TCP は TLS サーバーに通らない)
TLS サーバーに TLS 非対応のクライアントが繋いでも、平文 TCP にフォールバックされることはありません。
実際に試してみましょう。
ターミナル A(Step02 サーバーを起動)
go run ./tutorial/step02-tls/serverターミナル B(Step01 クライアントをポート 9443 に向けて接続)
# Step01 クライアントのポートを一時的に 9443 に変えて実行するか、
# 以下のスクリプトで動作を確認できる
go run ./tutorial/step01-tcp-echo/client
# => 9000番ポートに繋ごうとするのでサーバーが起動していなければ失敗Step01 クライアントのアドレスを localhost:9443 に変えて実行すると、以下のような結果になります。
TCP connected to localhost:9443 ← TCP 接続は成功する
sent: hello from plaintext client ← 平文データを送れる
connection closed by server ← サーバーが即座に切断するなぜ切断されるか:
- TCP 3-way ハンドシェイクは成功するため、TCP レベルでは接続できます
tls.Listenで待っているサーバーは、接続後に受け取る最初のバイト列が TLS のClientHelloであることを期待します- 平文の
hello\nが来た瞬間に「これは TLS ではない」と判断し、コネクションを切断します - サーバー側のログには
tls: first record does not look like a TLS handshakeというエラーが記録されます
これは意図的な設計です。「TLS ポートで平文を受け入れてしまう」という脆弱性を防ぐため、TLS サーバーは TLS 以外の通信を一切受け付けません。ブラウザで
https://のサイトにポート 443 でhttp://として繋ごうとしたときに接続が切られるのと同じ仕組みです。
4. openssl でも検証してみる
openssl s_client -connect localhost:9443 -CAfile tutorial/step02-tls/certs/ca.crtVerify return code: 0 (ok) と出れば、Go のクライアントと同じ理屈で検証が通っています。出力末尾の Server certificate セクションで SAN が確認できます。
パケットダンプ
tcpdump で実際に観測した本ステップのハンドシェイクを tls-dump.txt
に置いてあります。ClientHello / ServerHello / Certificate などのレコードがどう流れているか、生のバイト列で確認できます。
用語ミニまとめ
- CA (認証局): 証明書に署名する権威。今回は自分で作ったルート CA。
- 証明書チェーン: 「葉 (server.crt) → 中間 CA → ルート CA」のように検証可能な署名の連鎖。今回は中間 CA なし。
- SAN (Subject Alternative Name): 証明書が「どのホスト名/IP を名乗ってよいか」のリスト。
- SNI (Server Name Indication): 1つの IP で複数ホストを捌くため、クライアントが ClientHello に乗せて送るホスト名。
RootCAs: クライアント側が信頼する CA 集合。Certificates: サーバー側が提示する証明書/鍵ペア。
次へ (Step03 で解決すること)
ここまでで「サーバーが本物か」は確認できました。しかし、サーバー側からは 「繋いでくる相手が誰か」を確認できていません。誰でも (CA さえ無視すれば誰でも) サーバーに繋げてしまいます。
Step03 では、クライアントにも証明書を出させて、サーバーがそれを検証する = mTLS にします。