SSHの暗号通信の仕組み
SSH の接続開始から暗号化通信の確立までの流れを RFC 準拠で解説します。
この記事はクライアントとサーバが送り合う値を詳しく見ていくことが目的で、暗号技術の基礎知識を前提としています。
基礎知識
SSH は、ホスト(サーバ)認証・機密性・完全性を提供するプロトコルです。
- 公開鍵暗号(暗号化、鍵交換、電子署名)
- 共通鍵暗号
- MAC(メッセージ認証コード)
- ハッシュ関数
を組み合わせることで、これらの機能を提供します。
※ ECDH/DH鍵交換を使う場合は前方秘匿性も提供されます。
これからSSHの仕組みを説明するにあたって、以下の用語を理解しておく必要があります。
ホストキー
ホストは電子署名の秘密鍵・公開鍵ペア(ホストキー)を少なくとも1つ所有していなければなりません。
ホストキーは、ホスト認証(クライアントが正しいサーバと通信していることの確認)のため、鍵交換中に使用されます。その仕組み上、クライアントはホストキーの公開鍵を事前に知っていなくてはなりません。
この公開鍵とサーバの紐づけには、二種類の方法があります。
- クライアントが公開鍵とサーバ名(IPアドレスやFQDNなど)を紐づけたローカルのDBを持つ(例:
~/.ssh/known_hosts
)。 - CA(認証局)による署名付き公開鍵証明書を使う。
1については、そもそもそのDBをどのように構築するかという問題があります。
一度でも正しい相手と接続できればその公開鍵をDBに保存しておけばよいですが、初回接続時は何かしら別の安全な手段を利用しなくてはなりません。
例えば GitHubの場合、公開鍵のフィンガープリントがWeb上で公開されています。
HTTPS経由で入手できるため、このフィンガープリントを信頼できるものとして扱うことができます。
※「HTTPS経由だから」という言い方はTLSを理解している方でないと正確な意味が伝わりきらないですが、ここはSSHの解説記事なのでこれ以上踏み込みません。
これ以降、公開鍵・秘密鍵という言葉がよく出てきますが、ホストキーのそれを指す場合は「ホストキーの公開鍵」「ホストキーの秘密鍵」というように表現します。
接続のセットアップ
SSHはクライアント側から接続を開始します。
プロトコルバージョンの交換
まず、お互いが識別子を送信しあう必要があります。識別子とは、以下の形式の文字列です。
SSH-protoversion-softwareversion SP comments CR LF
例:SSH-2.0-OpenSSH_9.8
-
protoversion
: 2025年2月現在、最新のプロトコルバージョンは2.0
です。 -
comments
: これはオプションです。もしcomments
がある場合は、SP
(スペース)でsoftwareversion
と区切られる必要があります。 -
CR
: キャリッジリターン -
LF
: ラインフィード
CR
より前の部分はクライアント・サーバそれぞれの識別子として扱われ、鍵交換で利用されます。
クライアント側はこの識別子のみを送信しますが、サーバはこれ以外の情報も一緒に送信することができます。大抵は次で説明するSSH_MSG_KEXINIT
メッセージを一緒に送信します。
このプロトコルバージョンの交換を終えたら、鍵交換が始まります。
鍵交換
鍵交換は、暗号化・MAC付与(or AEAD)で使う鍵の材料である共有シークレットを共有するための手順です。鍵交換の後、共有シークレットからハッシュ関数を使って鍵を生成します。
また、ホスト認証もここで同時に行われます。
※クライアント認証は暗号化通信の確立後に行われます。クライアント認証の公開鍵認証の仕組みについては、拙記事SSHの公開鍵認証の仕組み(RFC準拠)をご覧ください。
アルゴリズム交渉
鍵交換は、まずお互いにサポートするアルゴリズムのリストを送信しあうことから始まります。
byte: SSH_MSG_KEXINIT(鍵交換の開始メッセージを表す値)
byte[16]: cookie(ランダムバイト列)
name-list: kex_algorithms(鍵交換アルゴリズム)
name-list: server_host_key_algorithms(サーバのホストキーのアルゴリズム)
name-list: encryption_algorithms_client_to_server(クライアントからサーバへの暗号化アルゴリズム)
name-list: encryption_algorithms_server_to_client(サーバからクライアントへの暗号化アルゴリズム)
name-list: mac_algorithms_client_to_server(クライアントからサーバへのメッセージ認証コードアルゴリズム)
name-list: mac_algorithms_server_to_client(サーバからクライアントへのメッセージ認証コードアルゴリズム)
name-list: compression_algorithms_client_to_server(クライアントからサーバへの圧縮アルゴリズム)
name-list: compression_algorithms_server_to_client(サーバからクライアントへの圧縮アルゴリズム)
name-list: languages_client_to_server(クライアントからサーバへの言語)
name-list: languages_server_to_client(サーバからクライアントへの言語)
boolean: first_kex_packet_follows(この次に推測された鍵交換メッセージが続くかどうか)
uint32: 0(将来拡張機能のためのフィールド)
name-listは、カンマ区切りで複数のアルゴリズムを優先度が高いものから列挙します。
cookie
は、送信者が生成したランダムな値です。これは、この後生成する鍵やセッション識別子をクライアント・サーバのどちらもコントロールできないようにするために必要です。
※ SSH_MSG_KEXINIT
メッセージはセッション識別子や後述するExchange Hashの生成に使われます。鍵はセッション識別子とExchange Hashを使います。
このメッセージの後、鍵交換アルゴリズムが実行されます。
以降、何かハッシュ計算を行う場合はkex-algorithms
で合意されたハッシュ関数を使います。
※ 鍵交換はホスト認証のためにハッシュ関数を使います。そのため、diffie-hellman-~~-sha256
やrsa2048-sha256
のようにアルゴリズム名でハッシュ関数が指定されています。
鍵交換(ECDH)
ここでは、ECDH(Elliptic Curve Diffie-Hellman)鍵交換を例に取ります。
DH鍵交換の場合も大体同じです。
交換アルゴリズムの概要は以下の通りです(RFC5656より)。
Client Server
------ ------
使い捨ての鍵ペアを生成
SSH_MSG_KEX_ECDH_INIT -------------->
受け取った鍵が正当なものか検証
使い捨ての鍵ペアを生成
共有シークレットを計算
Exchange Hashと署名を生成
<------------- SSH_MSG_KEX_ECDH_REPLY
受け取った公開鍵が正当なものか検証
受け取ったホストキーの公開鍵がサーバのものと一致するか検証
共有シークレットを計算
Exchange Hashを生成
署名を検証
詳しく見ていきます。
まず、クライアントはECDHの秘密鍵S_C
と公開鍵Q_C
を生成します。
そして、以下の形式のメッセージをサーバに送信します。
byte: SSH_MSG_KEX_ECDH_INIT(ECDH鍵交換メッセージの開始を表す値)
string: Q_C(クライアントの公開鍵)
サーバは、クライアントの公開鍵が有効かどうかを検証し、自身のECDHの秘密鍵S_S
と公開鍵Q_S
を生成します。
「公開鍵を検証」の意味
ECDH鍵交換の場合、公開鍵は楕円曲線上の点です。この点が有効なものであることを、以下の仕様の3.2.2の手順に従って検証します。
- Standards for Efficient Cryptography Group (SECG), "SEC 1: Elliptic Curve Cryptography", Version 2.0, May 2009. [Online]. Available: https://www.secg.org/sec1-v2.pdf
DH鍵交換の場合は、公開鍵が$[1,p-1]$の範囲にあるかどうかを検証します。$p$はDH鍵交換で使われる素数で、公開値(RFC3526で指定されている)です。
そして、共有シークレットK
をクライアントの公開鍵Q_C
とサーバの秘密鍵S_S
を使って計算します。
その後、Exchange HashH
を以下の連結のハッシュ値として計算します。
※ このExchange Hashはこの後よく登場します。
string: クライアントの識別子(プロトコルバージョン文字列)
string: サーバの識別子(同上)
string: クライアントからサーバへ送ったSSH_MSG_KEXINITメッセージのペイロード
string: サーバからクライアントへ送ったSSH_MSG_KEXINITメッセージのペイロード
string: サーバのホストキー
string: Q_C
string: Q_S
mpint: K
そして、このH
に対してホストキーの秘密鍵を使って署名signature
を生成します。
最後に、サーバは以下の形式のメッセージをクライアントに送信します。
byte: SSH_MSG_KEX_ECDH_REPLY(ECDH鍵交換の返信メッセージを表す値)
string: ホストキーの公開鍵
string: Q_S
string: signature
クライアントは、まず受け取った公開鍵が有効かどうかを検証します。
次に、受け取ったホストキーの公開鍵がサーバの正しいホストキーの公開鍵であることを検証します(例えば、前述したローカルDB~/.ssh/known_hosts
を使って)。
そして、共有シークレットK
と Exchange HashH
を生成します。
最後に、ホストキーの公開鍵を使って署名signature
を検証します。検証が成功すれば、正しいサーバと通信していることが確認できます。
なぜ「正しいサーバと通信している」といえるのか
署名対象に鍵交換の公開鍵(クライアント・サーバ両方)と共有シークレットが含まれているからです。これにより、署名を行った相手に対して間違いなく鍵交換が成立していることが分かります。鍵交換が成立すれば、これから暗号通信を行う相手が正しいサーバであるということが保証されます。
※ お互いの識別子やSSH_MSG_KEXINIT
、ホストキーの公開鍵も署名対象に含まれるので、これらのやり取りも正しい相手と行えていたことが保証されます。
※ 繰り返しになりますが、「サーバの正しい公開鍵を知っていること」が前提です。
また、これは鍵交換の欠点を補います。ECDH/DH鍵交換や後述するRSA鍵配送は優れた鍵交換手法ですが、これらの手法は鍵交換の相手が正しいものであることを保証しません。署名を組み合わせることでこの問題が解決されます。
鍵交換(RSA鍵交換の場合)
あまりRSA鍵交換は使われないですが、一応まだ非推奨ではない(MAY) ので説明します。
みんな大好きな「公開鍵で鍵を暗号化して、秘密鍵で復号する」やつです。このタイプの鍵交換を「使われていない」と言いきれなくさせる要因の一つなので、いい加減[SHOULD NOT]にしてほしいですね。する理由がまだないのでしょうが、、、
ここでのRSAは鍵交換のためのRSAで、暗号化なのでRSAES-OAEP[RFC3447]のことです。以降、RSAES-OAEPは単にRSAと呼びます。
また、現在SSHで使える鍵長は2048ビット、ハッシュ関数はSHA-256ですので、これも前提とします。
この鍵交換では、まずサーバが下記の形式のメッセージをクライアントに送信します。
byte: SSH_MSG_KEXRSA_PUBKEY(RSA鍵交換メッセージの開始を表す値)
string: サーバのホストキーの公開鍵
string: K_T(RSA公開鍵)
次に、クライアントは$0<=K<2^{LEN(key) - 2 * LEN(hash) - 49}$を満たす乱数K
を生成します。
※ RSAES-OAEPで暗号化可能なサイズより少し小さいですが、これはK
がmpint
エンコードされており、このエンコーディングで少しデータサイズが増えるためです。
ここで、LEN(key)
はRSAの鍵長(2048ビット)、LEN(hash)
はハッシュ関数の出力長(SHA-256なので256ビット)です。
その後、K_T
を使ってK
を暗号化し、以下の形式のメッセージをサーバに送信します。
byte: SSH_MSG_KEXRSA_SECRET(RSA鍵交換メッセージの続きを表す値)
string: 暗号化されたk
サーバは、受け取った暗号文をRSA秘密鍵で復号し、共有シークレットK
を取得します。
その後、以下の値の連結をハッシュ化したものをExchange HashH
とします。
string: クライアントの識別子(プロトコルバージョン文字列)
string: サーバの識別子(同上)
string: クライアントからサーバへ送ったSSH_MSG_KEXINITメッセージのペイロード
string: サーバからクライアントへ送ったSSH_MSG_KEXINITメッセージのペイロード
string: サーバのホストキー
string: RSA公開鍵
string: 暗号化されたk
mpint: K(共有シークレット)
そして、このH
に対してホストキーの秘密鍵を使って署名signature
を生成し、以下の形式のメッセージをクライアントに送信します。
byte: SSH_MSG_KEXRSA_DONE (RSA鍵交換の終了を表す値)
string: signature
クライアントはH
を計算し、サーバから受け取った署名signature
を検証します。検証が成功すれば、正しいサーバと通信していることが確認できます。
鍵交換からの出力
鍵交換により、共有シークレットK
と Exchange HashH
がそれぞれで生成されました。
セッションの最初の鍵交換で作られたH
はセッション識別子です。後述しますが、鍵交換はセッション中に複数回行われる場合があります。それでもセッション識別子は変わりません。
K
はまだ鍵の「素」であり、このままでは使いません(そもそも鍵の数が足りません)。
ここからK
やH
、ハッシュ関数を使って鍵を生成します。
鍵生成
鍵生成は、SSH独自の方法で行われます。
以降、K
を共有シークレット、HASH
をkex_algorithms
で合意されたハッシュ関数、H
をExchange Hash、session_id
をセッション識別子とします。
生成しなくてはならない値は、
- 暗号化で使うIV(Initialization Vector)
- 暗号化の鍵
- MACの鍵
をクライアントからサーバへの通信とサーバからクライアントへの通信のそれぞれの分です。
※ ただし、AES-128-GCMやChaCha20-Poly1305のようなAEADではMAC用の鍵を使いません。
以下のようにハッシュ関数を使って生成します。
- IV(server to client):
HASH(K || H || "A" || session_id)
- IV(client to server):
HASH(K || H || "B" || session_id)
- encryption key(server to client):
HASH(K || H || "C" || session_id)
- encryption key(client to server):
HASH(K || H || "D" || session_id)
- MAC key(server to client):
HASH(K || H || "E" || session_id)
- MAC key(client to server):
HASH(K || H || "F" || session_id)
"A","B",...は一文字のASCII文字です。
必要な長さが出力より短い場合、先頭から必要な長さ分を使います。
逆に、必要な長さが出力より長い場合、以下のように連結してハッシュ化したKey
を使います。
Key := K1 || K2 || K3 || ...
K1 := HASH(K || H || X || session_id)
(Xの部分は、必要とされる鍵で使われた文字。server to clientのIVなら"A")
K2 := HASH(K || H || K1)
K3 := HASH(K || H || K2)
...
鍵交換の終了
鍵交換を終えたら、以下のメッセージを送り合い、以降の通信は暗号化・MAC付きで行われます。
byte: SSH_MSG_NEWKEYS(新しい鍵を使うことを表す値)
再鍵交換
RFC4253によると、送信データの1GBごと または 接続時間の1時間ごと のいずれか早い方で鍵交換をもう一度行うことが推奨されています。
鍵交換中は、交換の開始時に有効な暗号化を利用して実行されます。
SSH_MSG_NEWKEYSが送信されたら、新しい暗号化・MACが使われるようになります。
参考文献
- RFC 3526 - More Modular Exponential (MODP) Diffie-Hellman groups for Internet Key Exchange (IKE)
- RFC 4251 The Secure Shell (SSH) Protocol Architecture
- RFC 4253 The Secure Shell (SSH) Transport Layer Protocol
- RFC 4432 RSA Key Exchange for the Secure Shell (SSH) Transport Layer Protocol
- RFC 5656 - Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer
- RFC 5647 - AES Galois Counter Mode for the Secure Shell Transport Layer Protocol
- RFC 8268 - More Modular Exponentiation (MODP) Diffie-Hellman (DH) Key Exchange (KEX) Groups for Secure Shell (SSH)
- The chacha20-poly1305@openssh.com authenticated encryption cipher draft-josefsson-ssh-chacha20-poly1305-openssh-00
- IANA Secure Shell (SSH) Protocol Parameters