はじめに
この記事はAWSに自作アプリをデプロイすることを目的とした学習の記録です。
筆者の自己紹介
- 異業種、実務未経験からフルスタックエンジニアを目指して転職活動中
- 2024年10月から学習を開始、オンラインスクール卒業を経て、現在も学習を継続中
ポートフォリオアプリ:https://github.com/geek-3340/Fameal_app/tree/main
前回までの記事:
今回の内容
前回まではLinuxコマンド、ネットワーク基礎、SSH接続についてまとめてきました。
今回はHTTPSプロトコルの安全性、盗聴・改ざん・なりすましを防止するための仕組みについて解説していきます。
内容は概念的な理解に留めた解説となっておりますので、より深く理解したい方は、前回の記事の最後に紹介した 「ネットワークはなぜつながるのか」 という書籍などで分かりやすく解説されているので、おすすめいたします!
HTTPSの必要性
HTTPとは
HTTP(HyperText Transfer Protocol) とは、WEBページ・WEBアプリに関するデータをやり取りするためのプロトコルで、クライアントが URL でリソースを要求し、サーバーがそれを返す、という「リクエスト・レスポンス」の仕組みで通信をおこなう
HTTPではWEBページのリソースとなる様々な種類のデータを運ぶ
- text / HTML ファイル
- 画像・動画・音声ファイル
- APIのデータ
など
パケットとは
通信する際に送る(あるいは返される)情報の塊をパケットと言い、パケットには上記のようなデータ本体以外に、HTTPメソッドやリクエスト先のアプリ等のホスト名など、さまざまな情報が格納されている
HTTPSとは
HTTPS(HTTP over TLS) とは、HTTPの通信をTLSプロトコルで暗号化したもの
なぜ暗号化が必要なのか?
HTTPでは平文のパケットでサーバーと通信をおこなう
例えば提示版アプリにログインするとき、以下のようなパケットがネットワークを流れる
POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
username=hijiri&password=mySecret123
カフェのWi-Fiでこれを送ったら、同じWi-Fiに繋いでる悪意ある第三者に中身を全部読まれ、パスワードが丸見えに...
HTTPS ではパケットの中身を暗号化し、途中で誰かが傍受しても、鍵で複号しない限り中身は意味不明な文字列にしか見えない
HTTPSが解決する3つの問題
HTTPS通信では「暗号化」に加え、「完全性検証」「認証」がおこなわれている
それぞれ以下のような問題を解決する
| 問題 | 解決手段 |
|---|---|
| 盗聴:通信内容の傍受 | 暗号化 (Confidentiality) |
| 改ざん:通信内容の書き換え | 完全性検証 (Integrity) |
| なりすまし:偽サーバーへ繋がされる | 認証 (Authentication) |
盗聴対策だけなら「暗号化」で対策できるが、他にも以下のような問題がある
- 通信内容が書き換えられていないか
- 現在接続しようとしているgoogle.comは本物か(DNSハイジャックされていないか)
なりすましの怖さ
仮にDNSハイジャックで google.com が悪意あるサーバーに向いていて、そのサーバーが本物のGoogleと完全に同じ見た目のログインページを返してきたら、ユーザーは騙されてパスワードを入力してしまうことも...
「暗号化されてるから安全」というのは、通信相手が本物であることが前提
HTTPSではサーバーの証明書を検証をすることで、この問題を解決している
HTTPS通信の仕組み
app.comというWEBアプリにHTTPS通信で接続するケースを解説する
証明書・認証局とは
まずHTTPS通信を行うための前提として、app.comのサーバーは自身の公開鍵とドメインを紐づける証明書を認証局から発行してもらう必要がある
認証局について
認証局(CA = Certificate Authority) とは「この公開鍵はあのドメインのものです」と保証する第三者機関
またブラウザやOSには世界中の信頼できる認証局の公開鍵があらかじめ組み込まれている
ためしにUbuntu等で以下のコマンドを実行
ls /etc/ssl/certs/ | head -20
→ たくさんの .pem ファイルが出力される
これらは世界中の信頼できる認証局たちの公開鍵でUbuntuに最初から入っており、ブラウザも同様の仕組みを持ってる
つまり信頼の起点は、
ユーザーは [ブラウザ/OS] を信頼する
↓
[ブラウザ/OS] は [リストに載っている認証局] を信頼する
↓
[認証局] は [サーバー証明書] に署名して保証する
↓
ユーザーは [サーバー証明書] を信頼できる
これを信頼の連鎖(Chain of Trust) という
証明書の発行
証明書は以下の手順で発行される
- サーバー運営者が自分のサーバー上で鍵ペアを生成
- 公開鍵とドメイン名などをまとめた CSR(証明書署名要求)に、自分の秘密鍵で署名「この鍵を確かに持っている」ことを示しつつ、認証局へ送る
- 認証局はドメインの検証(DNSやHTTPでの確認)を行ったうえで、「この公開鍵はこのドメインのものだ」という内容の証明書を作り、認証局の秘密鍵で署名して発行(公開鍵は前述の通りブラウザやOSに共有されている)
ここの時点で以下の2つのキーペアがある
- 認証局が元々持っているキーペア
- CSR時にサーバーで生成するキーペア
後述の説明でこれらが混同しないよう「認証局の公開鍵」[サーバーの秘密鍵]のように説明をする
証明書チェーンとは
厳密にいうとサーバーはルート認証局(世界中の信頼できる認証局)と直接やり取りはおこなわず、間に中間認証局が入っている
なぜこんな構造か?
- ルートCAの秘密鍵は超重要で万が一にでも漏れないようにするため
- 日常の証明書発行は中間CAがやる
- 万一中間CAが侵害されても、ルートまでは無事
ブラウザは証明書チェーン全体を辿って、最終的にOSに登録されているルートCAにたどり着けば「OK、信頼できる」と判断する
TLSハンドシェイク( RSA / DHE モデル )について
証明書の準備ができたら、ブラウザとサーバーが暗号化通信を始めるための「鍵合わせ」を行う
これをTLSハンドシェイク と呼ぶ
ハンドシェイクのゴールは、両者だけが知っている共通鍵(対称鍵)を安全に用意すること、あとはその鍵で高速に暗号化通信ができる
ではその共通鍵を、盗聴されているかもしれないネットワーク上でどうやって安全に共有するのか? この「鍵交換」の方法には、歴史的に大きく2つの方式がある。
- RSAモデル:共通鍵の素を[サーバーの公開鍵]で暗号化して送る(古い方式・TLS 1.2 以前)
- DHE(ECDHE)モデル:鍵そのものは送らず、両者が計算で同じ鍵にたどり着く(現代の主流・TLS 1.3)
以下で両方の流れを順番に見ていく。
なお、ここから登場する鍵は前述の通り「認証局のキーペア」と「サーバーのキーペア」の2組があるので、混同しないよう注意
本記事では「認証局の公開鍵」と「サーバーの秘密鍵・公開鍵」を区別して表記する
HTTPS通信フロー(RSAモデル)
- ブラウザ:
ClientHelloを送る(対応するTLSバージョンや暗号スイートの一覧を提示する) - サーバー:
ServerHelloを返し、証明書を提示する(証明書の中にサーバーの公開鍵が入っている) - ブラウザ:
認証局の公開鍵で証明書の署名を検証する(=本物の証明書か?を確認) - ブラウザ:共通鍵を生成し、証明書にある
サーバーの公開鍵で暗号化して送る - サーバー:
サーバーの秘密鍵で復号して共通鍵を取り出す(これで両者が同じ共通鍵を持つ) - 以降は共通鍵による対称暗号で高速通信を開始
解説
ポイントは 4〜5 の部分。共通鍵をサーバーの公開鍵で暗号化して送り、それをサーバーの秘密鍵で復号している
「公開鍵で暗号化したものは、対応する秘密鍵でしか復号できない」という非対称暗号の性質を使うことで、途中で傍受されても中身が共通鍵だとは分からない、という仕組みになっている
ただしこのモデルには 前方秘匿性(Forward Secrecy)がない という弱点がある
サーバーの秘密鍵が「身元の証明」と「共通鍵の解錠」を兼ねているため、もし通信を丸ごと記録されたうえで、将来サーバーの秘密鍵が漏えいすると、過去の通信まで遡って復号されてしまう
この弱点が理由で、RSAによる共通鍵の複合は TLS 1.3 で廃止された(身元証明は継続)
HTTPS通信フロー(DHEモデル)
- ブラウザ:
ClientHelloを送る(暗号スイートに加え、自分の「鍵シェア(公開値A)」を同梱する) - サーバー:
ServerHelloを返す(自分の「鍵シェア(公開値B)」を送る) - 両者:相手の公開値と自分の秘密値を組み合わせ、各自で同じ共通鍵を計算する(共通鍵そのものはネットワークに流れない)
- サーバー:証明書と、ここまでのやり取り全体に
サーバーの秘密鍵で署名したCertificateVerifyを、一緒にブラウザへ送るCertificateVerify = その接続でClientHello から直前の Certificate メッセージまで、交わした全メッセージを連結してハッシュ化したもの
- ブラウザ:2段階で身元を検証する
-
認証局の公開鍵で証明書を検証する(=この証明書は本物で、サーバーの公開鍵は確かにこのドメインのものか?) - 証明書の中の
サーバーの公開鍵でCertificateVerifyの署名を検証する(=いま通信している相手は、本当にそのサーバーの秘密鍵の持ち主か?)
-
- 以降は共通鍵による対称暗号で高速通信を開始
解説
DHE / ECDHE では、共通鍵を暗号化して送るのではなく、両者が公開値を交換して、各自で同じ鍵を計算する
ざっくり言うと、ブラウザは「自分の秘密値 × 相手の公開値」、サーバーは「自分の秘密値 × 相手の公開値」を計算すると、数学的に同じ値にたどり着く
盗聴者は交換された公開値(AとB)を見ても、そこから元の鍵を逆算することは事実上不可能
つまり 共通鍵が回線を流れることが一度もない のが最大の特徴。
そしてこのモデルでは、サーバーの秘密鍵は「共通鍵の解錠」には一切使われず、署名(身元証明)だけに使われる(手順4)
鍵交換に使う秘密値は接続ごとの使い捨てなので、後からサーバーの秘密鍵が漏れても、過去の通信は守られる
これが 前方秘匿性(Forward Secrecy) であり、RSAモデルとの決定的な違いになっている
なぜ2段階で検証するのか(手順5の補足)
手順5の2つの検証は、見た目は似ているがまったく別の問いに答えている。ここが少し混乱しやすいので整理しておく。
| 検証 | 使う鍵 | 確かめていること |
|---|---|---|
| ① 証明書の検証 | 認証局の公開鍵 | この[サーバーの公開鍵]は確かに app.com のものだ、と認証局が保証しているか |
| ② CertificateVerify の検証 | サーバーの公開鍵 | いま接続している相手は、そのサーバーの秘密鍵を本当に持っている当人か |
ポイントは、①の署名は証明書の発行時に認証局が事前に付けた静的なものなのに対し、②の署名はサーバーがこの接続のためにその場で作った動的なものだということ
なぜ両方が必要かというと、証明書はただのデータなので、コピーすれば誰でも提示できてしまう
①だけでは「本物の証明書をコピーして提示しただけ」のなりすましを防げない
そこで②で「相手がサーバーの秘密鍵の持ち主であること」をその場で確認することで、初めて『本物のサーバーと話している』と確定できる
さらに CertificateVerify が署名する対象は、ここまでのハンドシェイクのやり取り全体(手順1〜2で交換した鍵シェアAやBも含む)であるため、署名はこの接続専用になり、過去の正規の署名を別の接続で使い回すリプレイ攻撃も防げる仕組みになっている
この方式が、現在の TLS 1.3 で標準 となっている
おわりに
今回は HTTPS がどのように「盗聴・改ざん・なりすまし」を防いでいるのか、その仕組みを中心にまとめました。
- 暗号化 で盗聴を防ぎ
- 完全性検証 で改ざんを防ぎ
-
証明書と認証局による認証 でなりすましを防ぐ
という3本柱で安全性が成り立っていること、そして暗号化に使う共通鍵を安全に共有するための鍵交換に、RSAモデルとDHE(ECDHE)モデルの2方式があることを見てきました。特に「認証局のキーペア」と「サーバーのキーペア」の役割の違いを押さえると、ハンドシェイクの流れがぐっと理解しやすくなると思います。
ご覧いただきありがとうございました🔥

