🐻‍❄️

証明書?OAuth?「なんとなく」で済ませてきた概念を整理してみた(セキュリティ編)

に公開

この記事について

前回のネットワーク編の記事。
https://zenn.dev/ncdc/articles/fbcc6e0899739f

今回はセキュリティ系の「なんとなく」になっていた概念について調べたことをまとめました。記事の目標は前回同様、「〇〇はこういうものでしょ?」とざっくり説明できる ようになること。全体の流れを掴むことです。


扱っているキーワード

キーワード ざっくり一言
共通鍵暗号と公開鍵暗号 暗号化の2つの方式。すべての土台
ハッシュ化 元に戻せない変換。改ざん検知やパスワード保護に使う
SSL/TLS証明書 サーバーの身分証明書
認証局(CA) 証明書を保証する第三者機関
OAuth 2.0 / OpenID Connect パスワードを渡さない認可・認証
SSH鍵 鍵ペアによる本人確認
CORS ブラウザによる別ドメインへのアクセス制御

これらが下のようにつながっていることがわかりました。


1. 共通鍵暗号と公開鍵暗号

まず最初に調べたのが暗号化の仕組みです。SSL/TLSもSSH鍵も、結局のところ暗号化の話なので、ここがわからないと先に進めませんでした。

遭遇するとき

  • 「HTTPSは暗号化されてるから安全」→ どう暗号化してるの?
  • 「公開鍵と秘密鍵のペアを作って」→ なんで2つあるの?
  • 「AES-256で暗号化しています」→ AESって何?

一言でいうと

  • 共通鍵暗号 = 同じ鍵で暗号化・復号する。速いけど鍵の受け渡しが課題
  • 公開鍵暗号 = 別々の鍵(公開鍵・秘密鍵)を使う。遅いけど鍵の受け渡しが安全

もう少し詳しく

共通鍵暗号(対称鍵暗号)

送信者 →【共通鍵で暗号化】→ 暗号文 →【同じ共通鍵で復号】→ 受信者

同じ鍵を使うのでシンプルで高速。代表的なのが AES(Advanced Encryption Standard) で、「AES-256で暗号化」のような表記はこれのことです。でも 「その鍵をどうやって安全に相手に渡すか」 が問題になります。

公開鍵暗号(非対称鍵暗号)

送信者 →【相手の公開鍵で暗号化】→ 暗号文 →【自分の秘密鍵で復号】→ 受信者

公開鍵は全世界に公開してOK。秘密鍵だけ守ればいいので、鍵の受け渡し問題が解決します。ただし処理が遅くなるらしい

実際のHTTPS通信では両方使う

ここが個人的に一番「なるほど」と思ったポイントです。

1. 公開鍵暗号で「共通鍵」を安全に交換する(遅いけど1回だけ)
2. 以降は共通鍵暗号で通信する(速い)

いいとこ取りをしているのがSSL/TLS通信の仕組みらしいです。この仕組みを知っていると、後で出てくるSSL/TLS証明書やSSH鍵の話がスッと入ってきます。

覚えておくこと

項目 内容
共通鍵暗号 の代表 AES。現在の標準。速くて安全
公開鍵暗号 の代表 RSA、Ed25519。SSH鍵やSSL/TLSで使われる
HTTPS = 両方使う 公開鍵で鍵交換 → 共通鍵で通信。いいとこ取り
公開鍵はばらまいてOK 秘密鍵だけ絶対に守る

2. ハッシュ化

暗号化と一緒に出てきたのが「ハッシュ化」。正直、今まで暗号化と混同していました。調べてみたら全然違うものでした。

遭遇するとき

  • 「パスワードはハッシュ化して保存してるから大丈夫」→ ハッシュ化って暗号化と何が違うの?
  • 「ファイルのチェックサムを確認して」→ チェックサムって何?
  • 「SHA-256で署名しています」→ SHAって何?

一言でいうと

データを固定長の値に変換する「一方通行」の処理。元に戻せないのが最大の特徴。

もう少し詳しく

入力:  "password123"
  ↓ ハッシュ関数(SHA-256)
出力:  "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"

SHA(Secure Hash Algorithm) はハッシュ関数の代表格で、SHA-256は256ビットのハッシュ値を生成します。同じ入力からは必ず同じ出力が得られますが、出力から入力を逆算することはできません

暗号化との違い

自分が混同していたポイントをまとめると、

暗号化 ハッシュ化
元に戻せるか 戻せる(鍵があれば) 戻せない
用途 データの秘匿 改ざん検知、パスワード保護
出力の長さ 入力に依存 常に固定長

どこで使われている?

この記事で扱う他の概念とかなり関わっていました。

  • SSL/TLS証明書 → 証明書の署名にハッシュが使われている(改ざん検知)
  • パスワード保存 → DBにはパスワードのハッシュ値だけを保存する
  • JWT → トークンの署名部分にハッシュを使って改ざんを防止
  • ファイルのダウンロード → チェックサム(= ファイルのハッシュ値)を比較して、壊れずにダウンロードできたか確認

覚えておくこと

項目 内容
SHA-256 現在の標準的なハッシュ関数。証明書やJWTの署名に使われる
bcrypt / Argon2 パスワード保存用のハッシュ。意図的に遅くして総当たり攻撃を防ぐ
MD5 / SHA-1 は非推奨 衝突(異なる入力で同じ出力が出る)が見つかっている
ハッシュ化 ≠ 暗号化 「パスワードを暗号化して保存」は間違い。正しくは「ハッシュ化して保存」

3. SSL/TLS証明書

セクション1の暗号化を理解してからだと、仕組みがかなりわかりやすくなりました。

遭遇するとき

  • 「このサイト、証明書エラーが出てる」
  • 「証明書の更新作業お願い」

一言でいうと

「このサーバーは本物ですよ」を証明するデジタルな身分証明書。

もう少し詳しく

ブラウザで https:// のサイトにアクセスすると、裏側で以下のようなことが起きているようです。

ブラウザ → サーバー: 「あなた本当に example.com?」
サーバー → ブラウザ: 「はい、これが私の証明書です」(証明書を提示)
ブラウザ: 「この証明書は信頼できる認証局が発行してるな...OK、信頼しよう」
(ここから暗号化通信が始まる)

つまりSSL/TLS証明書には2つの役割があります。

  1. このサーバーが本物であることの証明(認証)
  2. 通信を暗号化するための鍵の交換(暗号化)— セクション1で調べた「公開鍵で共通鍵を交換」がまさにここで使われています

覚えておくこと

項目 内容
証明書には有効期限がある Let's Encryptは90日、商用は1年が多い
期限切れ = サイトにアクセスできなくなる ブラウザが警告を出してブロックする
SSL は古い。今は TLS でも慣習で「SSL証明書」と呼ばれ続けている
Let's Encrypt = 無料で証明書を発行してくれる認証局 ACMやcertbotで自動更新するのが一般的
AWS ACM(Certificate Manager)を使えば自動更新される ALBやCloudFrontとの組み合わせで証明書管理が不要に

開発で困ったときの対処

# 証明書の有効期限を確認
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates

# ローカル開発で証明書エラーが出たら(※本番ではやらない)
# Node.js
NODE_TLS_REJECT_UNAUTHORIZED=0 node app.js

# curl
curl -k https://localhost:8443

4. 認証局(CA)

証明書を調べていると必ず出てくるのが「認証局」。証明書が身分証明書なら、認証局はそれを発行する役所みたいなものでした。そして、ここでセクション2のハッシュ化がまた出てきます。

遭遇するとき

  • 「社内のCAで発行した証明書を使って」
  • 「ルート証明書を信頼ストアに追加して」
  • 「この証明書、オレオレ証明書じゃない?」

一言でいうと

「この証明書は本物ですよ」と保証してくれる第三者機関。

もう少し詳しく

証明書は自分で作ることもできます(自己署名証明書、通称「オレオレ証明書」)。でも自分で「俺は本物だ」と言っているだけなので、誰も信頼してくれません

そこで、ブラウザやOSがあらかじめ「この機関は信頼する」と決めている組織があります。それが認証局(Certificate Authority = CA) です。

信頼の連鎖(チェーン)

ルート認証局(OSやブラウザに組み込み済み)
  └── 中間認証局(ルートが「こいつは信頼できる」と保証)
        └── あなたのサーバー証明書(中間CAが「こいつは信頼できる」と保証)

ブラウザはこの連鎖をたどって、最終的にルート認証局にたどり着ければ「OK、信頼しよう」と判断します。

ここで面白いのが、認証局は証明書にデジタル署名(秘密鍵 + ハッシュ)を行っているということ。ブラウザは認証局の公開鍵で署名を検証し、証明書が改ざんされていないことを確認します。セクション1の公開鍵暗号とセクション2のハッシュ化が組み合わさっているんですね。

覚えておくこと

項目 内容
ルート証明書 OSやブラウザに最初から入っている。信頼の起点
中間証明書 サーバー証明書の設定時に一緒に入れ忘れると、PCでは見えるのにスマホで見えないなどの問題が起きる
自己署名証明書 開発環境ではOK、本番では使わない
Let's Encrypt 無料の認証局。個人・小規模サイトで広く利用

「中間証明書のインストール忘れ」問題

調べていて「これ絶対ハマるやつだ」と思ったのがこれです

  • PC(Chrome) → 正常に表示される
  • スマホ(Safari) → 証明書エラー

原因は、PCのブラウザが中間証明書をキャッシュしていて、スマホにはキャッシュがないから。サーバーに中間証明書を正しく設定すれば解決するとのことです。


5. OAuth 2.0 / OpenID Connect

「Googleでログイン」は何度も実装したことがあるのに、中身をちゃんと理解していなかったので改めて調べました。調べてみたら、ここでもハッシュ化やSSL/TLSが裏で使われていて、やっぱり全部つながってるんだなと思いました。

遭遇するとき

  • 「Googleでログイン」を実装する
  • 「OAuthのアクセストークンが切れた」
  • 「認証と認可の違いって何?」

一言でいうと

  • OAuth 2.0 = 「あのサービスのこの機能を使っていいよ」という**認可(許可)**の仕組み
  • OpenID Connect = OAuth 2.0の上に「この人は誰か」を確認する認証を載せたもの

もう少し詳しく

「Googleでログイン」ボタンを押したときの流れ

1. あなたのアプリ → Google: 「このユーザーの情報をください」
2. Google → ユーザー: 「このアプリにメールアドレスを共有していい?」
3. ユーザー → Google: 「いいよ」
4. Google → あなたのアプリ: 「認可コードです」
5. あなたのアプリ → Google: 「この認可コードでトークンください」(←この通信はSSL/TLSで暗号化)
6. Google → あなたのアプリ: 「アクセストークンとIDトークンです」

ポイント:あなたのアプリはユーザーのGoogleパスワードを一切知りません。

認証と認可の違い

混同していたのでここで整理。

認証(Authentication) 認可(Authorization)
質問 あなたは誰? 何をしていい?
ログイン 「写真フォルダへのアクセスを許可」
仕組み OpenID Connect OAuth 2.0
トークン IDトークン(JWTが多い) アクセストークン

覚えておくこと

項目 内容
アクセストークン APIを呼ぶための「入場券」。有効期限が短い(数分〜数時間)
リフレッシュトークン アクセストークンを再発行するための「引換券」。有効期限が長い
IDトークン(JWT) ユーザーの情報が入っている。Base64デコードすれば中身が読める(暗号化ではない)
スコープ 「何を許可するか」の範囲。email, profile, read:repos など

JWTの中身を見てみる

IDトークンはこんな形式です

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NSIsImVtYWlsIjoiZnVtaUBleGFtcGxlLmNvbSJ9.signature

. で3つに分かれていて、真ん中をBase64デコードすると、

{
  "sub": "12345",
  "email": "fumi@example.com",
  "name": "Fumi",
  "iat": 1700000000,
  "exp": 1700003600
}

3つ目の signature 部分は、ヘッダーとペイロードをハッシュ化して秘密鍵で署名したものです。ここでもセクション2のハッシュ化が使われていました。これにより「このトークンは改ざんされていない」ことが検証できます。

Base64は暗号化ではありません。 誰でも中身を読めるので、JWTに機密情報を入れてはいけません。


6. SSH鍵

最後はSSH鍵。GitHubの設定でなんとなく作っていたけど、セクション1の公開鍵暗号をそのまま使った仕組みだとわかって、ようやく腑に落ちました。

遭遇するとき

  • GitHubに初めてpushしようとして拒否された
  • 「SSH鍵を登録してください」と言われた
  • Permission denied (publickey) エラー

一言でいうと

パスワードの代わりに「鍵ペア」で本人確認する仕組み。

もう少し詳しく

SSH鍵は秘密鍵公開鍵のペア。セクション1で調べた公開鍵暗号そのものです。

秘密鍵(id_ed25519)      → あなたのPCに保管。絶対に人に渡さない
公開鍵(id_ed25519.pub)  → GitHubなどのサーバーに登録する

仕組みを鍵と鍵穴でたとえると、

  • 公開鍵 = 鍵穴(サーバーに取り付ける)
  • 秘密鍵 = 鍵(自分だけが持つ)

公開鍵(鍵穴)はいくらばらまいても大丈夫ですが、秘密鍵(鍵)を渡したら誰でも入れてしまいます。

覚えておくこと

項目 内容
ed25519 を使おう RSAより短くて安全。ssh-keygen -t ed25519
秘密鍵は絶対に共有しない Slackで送る、Gitにコミットする → 全部NG
パスフレーズを設定する 秘密鍵ファイルが流出しても、パスフレーズがないと使えない
~/.ssh/config を活用する 複数の鍵を使い分けるときに便利

鍵の作成から登録まで

# 鍵ペアを生成
ssh-keygen -t ed25519 -C "your_email@example.com"

# 公開鍵の内容をコピー
cat ~/.ssh/id_ed25519.pub
# → これをGitHubの Settings > SSH keys に貼り付ける

# 接続テスト
ssh -T git@github.com
# → "Hi username! You've successfully authenticated" と出ればOK

7. CORS(Cross-Origin Resource Sharing)

暗号化やトークンとは少し毛色が違いますが、CORSもブラウザのセキュリティ機能なのでこちらにまとめます。フロントエンドからAPIを叩いたときの赤いエラー、あれの正体です。

遭遇するとき

  • フロントエンドからAPIを叩いたら赤いエラーが出た
  • Access-Control-Allow-Origin を設定しろと言われた
  • preflight という謎のリクエストが飛んでいた

一言でいうと

「別のドメインのリソースにアクセスしていいですか?」をブラウザがサーバーに確認する仕組み。

もう少し詳しく

ブラウザには同一オリジンポリシーというルールがあります。

https://myapp.com から https://api.example.com にリクエスト
→ オリジン(ドメイン)が違うのでブロック!

これはセキュリティのための仕組みで、悪意のあるサイトが、あなたのログイン済みサービスのAPIを勝手に叩くのを防いでいます。

でも、自分のフロントエンドから自分のAPIを叩きたいときも同じルールが適用されてしまいます。そこでCORSという仕組みで、「このオリジンからのアクセスはOKだよ」とサーバーが許可します。

ブラウザ → サーバー: 「https://myapp.com からアクセスしたいんですが」(OPTIONSリクエスト)
サーバー → ブラウザ: 「https://myapp.com はOKです」(Access-Control-Allow-Origin ヘッダー)
ブラウザ: 「許可されたな、じゃあ本番のリクエストを送ろう」(実際のGET/POSTリクエスト)

覚えておくこと

項目 内容
CORSエラーはブラウザが出す curl やPostmanでは発生しない。サーバーは正常にレスポンスを返している
OPTIONSリクエスト = preflight ブラウザが「先に確認していい?」と聞くリクエスト。自動で送られる
Access-Control-Allow-Origin: * 全オリジン許可。開発中はOK、本番では特定オリジンを指定すべき
Cookieを送る場合credentials: 'include'Access-Control-Allow-Credentials: true が必要 ワイルドカード * は使えなくなる

「CORS設定したのに動かない」あるある

1. サーバーで設定した → でもAPIゲートウェイが上書きしている
2. GET は動く → POST だけ動かない(preflightへの対応が漏れている)
3. Cookieが送られない → credentials設定が片方だけ

そもそもCORSを回避する方法

リバースプロキシ(ネットワーク編参照)を使えば、CORSを根本的に回避できます。

# リバースプロキシで同一オリジン配信
https://myapp.com/        → フロントエンド(静的ファイル)
https://myapp.com/api/    → バックエンドAPI(プロキシで転送)

→ 同じオリジンなのでCORSエラーは発生しない!

Access-Control-Allow-Origin: * で雑に解決しなくて良くなります。


まとめ

概念 一言まとめ つながり
共通鍵暗号・公開鍵暗号 暗号化の2つの方式 すべての土台
ハッシュ化 元に戻せない一方通行の変換 証明書署名、JWT、パスワード保護
SSL/TLS証明書 サーバーの身分証明書 公開鍵暗号 + 共通鍵暗号を使う
認証局(CA) 証明書を保証する第三者 ハッシュ + 公開鍵で署名
OAuth / OIDC パスワードを渡さずに認可・認証 JWTの署名にハッシュを利用
SSH鍵 鍵ペアで本人確認 公開鍵暗号そのもの
CORS ブラウザが別オリジンへのアクセスを制御 ブラウザのセキュリティ機能

全部を暗記しようと思うとそれなりに時間と労力がかかると思いますが、「証明書って何?」と聞かれたときに 「サーバーの身分証明書みたいなもので、認証局っていう第三者が保証してるんだよ」 くらいにざっくり説明できれば、この記事の目標は達成かなと思います。同じように「なんとなく」で済ませてきた方の参考になれば嬉しいです。

参考

NCDC テックブログ

Discussion