はじめに
一体何の話なんです?
SSL/TLSというのは、ご存じの通り、4種類の機能 Kx(鍵交換),Au(認証),Enc(暗号),Mac(ハッシュ/AEAD) を組み合わせたハイブリッド暗号技術です。
ここに、DHやECDHという鍵交換の方式が使われていることもご存じとは思いますが、ほとんどの場合それは、DHEやECDHEといったEphemeral(一時キー)版を指します。
しかし…。おそらく超マイナーながら、EphemeralでないDH,ECDH、通称static-DH,static-ECDHを使う方式もプロトコル上はあるのです。
ということで、今回はその中でもstatic-DHの方に焦点をあて、色々試してみた話です。
注意点
static-DH自体はFS(Forward Secrecy)が確保できないということで、今後は使われなくなります。というか、メジャーなブラウザでもはや対応していないようです。
そういう意味で、全く実用性はありませんので、悪しからずご了承ください。
基礎知識の整理
SSL/TLSにおける3種類の認証
SSL/TLSのKx,Auに使われるのは、kerberosのような他の認証に相乗りする方式を除けば、いわゆる**(広義の)公開鍵暗号つまり、公開鍵・秘密鍵という非対称な鍵を別々の操作に用いる方式ですが、もう少し細かく分類すると更に3種類に分かれます。それは、(狭義の)公開鍵暗号**、電子署名、鍵交換の3種類です。
- (狭義の)公開鍵暗号
公開鍵を暗号化、秘密鍵を復号に用いる技術であり、RSA暗号等が該当します。
TLSのCipher Suite (暗号の組み合わせ)としては TLS_RSA_WITH_AES_256_CBC_SHA256 等が該当し、この場合 RSA暗号が Kx,Au両方を兼ねます。 - 電子署名
秘密鍵を署名作成、公開鍵を署名検証に用いる技術であり、RSA署名、DSA、ECDSA等が該当します。※鍵を共通で使えるとは言え、RSA暗号とRSA署名は別物であることに注意
TLSのCipher Suiteとしては TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 等が該当し、この場合 RSA署名が Au に、そして後述の鍵交換の一つDHE(DH-Ephemeral)が Kx になっています。
逆に、電子署名単独で Kx を兼ねることはできないため、DHE あるいは ECDHE といった何らかの鍵交換との併用が必ず行われます。 - 鍵交換
RSA暗号のような(狭義の)公開鍵暗号を使う方式もありますが、ここでは2者でお互いの公開鍵を交換し、それぞれで秘密鍵と組み合わせることで秘密情報を共有する技術を指します。DH(E)やECDH(E)が該当します。
DHE,ECDHEに関しては上の電子署名でも出ましたが、実は鍵交換単独でKx,Auを兼用する方式もあり、これが今回の話題の核心です。TLSのCipher Suiteとしては、TLS_DH_RSA_WITH_AES_256_CBC_SHA256 等が該当します。前述のものと E の一文字がないだけのようですが、内実は大分違い、Kx=Au=DH ( static-DH ) の方式になっています。
鍵交換のEphemeral(一時鍵)とstatic(固定鍵)
この2つは鍵の用意の仕方、それのみの違いです。
Ephemeral(一時鍵)とは、鍵交換を行うたびに鍵 ( 公開鍵・秘密鍵の鍵ペア ) を使い捨てする方式を指し、文字通り一時的にしか鍵を使いません。
逆にstatic(固定鍵)は、(通常は両者ではなくサーバ側のみですが) 同じ鍵を使い続ける方法です。
FS ( Forward Secrecy ) の確保のため、今時使われるのは Ephemeral の方だけなのですが…FS についての詳細は、本記事では割愛します。
やってみた
テスト環境
今回はこのような環境で試しています。
- 64bit Windows10 + WSL(Windows Subsystem for Linux/Bash on Windows、Ubuntu 16.04.2)
- OpenSSL 1.0.2g on WSL
- Wireshark 2.0.2
- RawCap 0.1.5.0
なお、RawCapは特権(UAC)が必要になるプログラムですので、同じように試される場合にはご注意を、と、取り扱いは自己責任でお願いします。念のため。
概要
やったことの概要です。
- テスト用の認証局をたて、static-DHに必要な証明書を発行する
- OpenSSLのSSL/TLSテストサーバ機能を用いて、ブラウザ・サーバ間通信をシミュレートする
- 通信内容をキャプチャし、解析する
証明書の作成
認証局
ということで、まずは認証局をたてます。OpenSSL付属のCA.shやCA.plというスクリプト、或いはopensslコマンドのcaサブコマンドを使っても良いのですが、色々なファイル・フォルダ構造を意識する必要があるので、もっとプリミティブに行きます。
ということで、次のようなコマンドで作りました。
$ openssl req -config /usr/lib/ssl/openssl.cnf -new -newkey rsa:1024 -nodes -keyout ca.key -out ca.csr -subj "/CN=Six Gates Test CA/O=Soukai Synd./ST=Neo-Saitama/C=JP"
$ openssl x509 -signkey ca.key -days 10 -req -in ca.csr -out ca.crt -sha1 -extfile /usr/lib/ssl/openssl.cnf -extensions v3_ca
$ openssl x509 -serial -noout -in ca.crt | sed -e 's/.*=//' > ca.srl
要点をリストアップすると次の通りです。
- 秘密鍵
ca.key
、CA証明書ca.crt
、シリアル管理用のca.srl
の3ファイルを作った。
※シリアルはなくてもテストに支障はないのですが気分的に - 秘密鍵については、後々の操作が楽になるようにパスワード保護なし (
-nodes
) で作った。 - opensslのサブコマンド ca に合わせ、設定ファイルにある
v3_ca
のエクステンションを証明書に盛り込んでいる。
なお、RSA1024bitにSHA1と今時貧弱な方式を指定していますが、まあこれもテスト用なのでテキトーです。
証明書 ( DH用 )
続いてDHの鍵を作り、それを元に証明書を発行します。コマンドとしては次の通りです。なお、それに先立って設定ファイルtest.cnf
を作成しておきます。( 内容は後述 )
$ openssl dhparam -out dh.prm 1024
$ openssl genpkey -paramfile dh.prm -out dh.key
$ openssl pkey -in dh.key -pubout -out dh.pub
$ openssl req -config /usr/lib/ssl/openssl.cnf -new -newkey rsa:512 -nodes -keyout /dev/null -out dummy.csr -subj "/CN=dhtest.ns/O=Omura Industries MC./ST=Neo-Saitama/C=JP"
$ openssl x509 -req -in dummy.csr -out dh.crt -force_pubkey dh.pub -CAkey ca.key -CA ca.crt -days 5 -sha1 -CAserial ca.srl -extfile test.cnf -extensions test_cert
要点をリストアップすると次の通りです。
なお、公式な証明書の場合はWebサイト運用者とCAとの役割が分かれるわけですが、今回は手元で一緒くたにしていることに注意が必要です。
※CA鍵を使う処理はCAの、サーバ用の鍵を扱う処理はWebサイト運用者が行うべき処理です。
※Webサイト運用者からCAに渡すデータはCSRと公開鍵のみです。サーバの秘密鍵はCAには渡しません。
- opensslのdhparam,genpkey,pkey各サブコマンドでDHの鍵ペアを作る。今回1024bitと今時(略
- 電子署名として使えないDHの鍵ではCSRが作れないため、ダミーのRSA鍵を作り、それを元にCSRを作る。
- CA鍵により署名を行い、証明書
dh.crt
を作成する。 - CA鍵により署名をする際に、証明書に盛り込む公開鍵を、
-force_pubkey
オプションで、今回のDH公開鍵に差し替える。 - opensslのcaサブコマンドの場合に標準で付与される
usr_cert
のエクステンションの代わりに、test.cnf
のtest_cert
のエクステンションを付与する。
なお、test.cnf
の内容は次の通りです。SSL/TLSサーバ証明書としてnsCertType
を、また、鍵交換に使うDH公開鍵を含めていますから、KeyUsage
としてKeyAgreement
を指定します。
[ test_cert ]
basicConstraints = CA:FALSE
nsCertType = server
keyUsage = keyAgreement
nsComment = "There is no mercy."
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
証明書に関して参考
証明書ですが、拡張子.crt
で作っているため、単にダブルクリックして開くだけで、Windowsの証明書ビューアで内容を確認することができます。次の通りDHの公開鍵が含まれていることが分かります。
なお、上で挙げた手順を一括で行うスクリプトです。
#!/bin/bash
BIN=openssl
CNF=/usr/lib/ssl/openssl.cnf
cat > test.cnf <<'_EOS_'
[ test_cert ]
basicConstraints = CA:FALSE
nsCertType = server
keyUsage = keyAgreement
nsComment = "There is no mercy."
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
_EOS_
rm -f ca.* dh.* dummy.csr
$BIN req -config $CNF -new -newkey rsa:1024 -nodes -keyout ca.key -out ca.csr -subj "/CN=Six Gates Test CA/O=Soukai Synd./ST=Neo-Saitama/C=JP"
$BIN x509 -signkey ca.key -days 10 -req -in ca.csr -out ca.crt -sha1 -extfile $CNF -extensions v3_ca
$BIN x509 -serial -noout -in ca.crt | sed -e 's/.*=//' > ca.srl
$BIN dhparam -out dh.prm 1024
$BIN genpkey -paramfile dh.prm -out dh.key
$BIN pkey -in dh.key -pubout -out dh.pub
$BIN req -config $CNF -new -newkey rsa:512 -nodes -keyout /dev/null -out dummy.csr -subj "/CN=dhtest.ns/O=Omura Industries MC./ST=Neo-Saitama/C=JP"
$BIN x509 -req -in dummy.csr -out dh.crt -force_pubkey dh.pub -CAkey ca.key -CA ca.crt -days 5 -sha1 -CAserial ca.srl -extfile test.cnf -extensions test_cert
通信のシミュレート
Webサーバのシミュレート
opensslにはs_serverというサーバテスト用のサブコマンドが用意されていますので、こちらを使います。これに-WWW
というオプションを渡すと、URLで指定されたファイルをそのまま返す簡易Webサーバの出来上がりです。
$ openssl s_server -cert dh.crt -key dh.key -WWW
-cert
,-key
オプションにより、作成した証明書と秘密鍵を指定します。他にもオプションは色々あるのですが、特に指定しません。この場合、デフォルトのTCP4433番ポートでlistenします。
なお、-WWW
を指定した場合、opensslコマンドは延々リクエストを待ち続けますので、止める時はCtrl-Cなりkillコマンドなりで止めます。また、クライアント側は別ターミナルで操作した方がやり易いと思います。
wgetによるクライアント側テスト
ではクライアント側ですが、第1弾としてwgetを使います。
ただ、今回証明書のコモンネームをdhtest.nsで作っていますので、できればその名前でアクセスしたいところです。そのため、WSL上での名前解決として、/etc/hostsに127.0.0.1 localhost dhtest.ns
というエントリを追加できるとより本格的です。なお、追加する場合はsudo等によりroot権限で行ってください。
ということで、サーバ側の動作ディレクトリにns.txt
というファイルを適当につくり、別ターミナルでwgetを実行します。
$ wget --ca-certificate=ca.crt -O download.txt https://dhtest.ns:4433/ns.txt
--2018-01-02 22:50:36-- https://dhtest.ns:4433/ns.txt
Resolving dhtest.ns (dhtest.ns)... 127.0.0.1
Connecting to dhtest.ns (dhtest.ns)|127.0.0.1|:4433... connected.
HTTP request sent, awaiting response... 200 ok
Length: unspecified [text/plain]
Saving to: ‘download.txt’
download.txt [ <=> ] 9 --.-KB/s in 0s
2018-01-02 22:50:36 (149 KB/s) - ‘download.txt’ saved [9]
CA証明書を指定してあげないとオレオレ証明書として弾かれますので注意が必要です。
また、今回はサーバ動作ディレクトリと同じディレクトリでwgetを実行していますので、-O
でダウンロードファイル名をオリジナルから変えました ( 変えなくても重複回避はしてくれるのですが )。以下のように同じ内容がダウンロードできていることが確認できます。
$ more ns.txt download.txt | cat
::::::::::::::
ns.txt
::::::::::::::
Wasshoi!
::::::::::::::
download.txt
::::::::::::::
Wasshoi!
ちなみに/etc/hosts
を弄らない場合は、証明書のコモンネームに沿ったURLが使えませんので、例えばlocalhost
等を使うわけですが、そうすると証明書不正と判断されてしまいます。
$ wget --ca-certificate=ca.crt -O download.txt https://localhost:4433/ns.txt
--2018-01-02 23:05:23-- https://localhost:4433/ns.txt
Resolving localhost (localhost)... 127.0.0.1
Connecting to localhost (localhost)|127.0.0.1|:4433... connected.
ERROR: certificate common name ‘dhtest.ns’ doesn't match requested host name ‘localhost’.
To connect to localhost insecurely, use `--no-check-certificate'.
その場合、エラーメッセージにヒントが出ていますが、証明書不正判定を握り潰すために--no-check-certificate
オプションを指定します。…まあ勿論これはテストの時だけにした方が良いオプションですが。
opensslのs_clientサブコマンドによるテスト
第2弾として、opensslのクライアントテスト機能であるs_clientサブコマンドです。なぜwgetで終わりにしないのかと言うと、サーバ側もwgetも、通信の解析に必要な十分量のデータを残してくれないからです…。
s_clientは、通信内容を対話的に入出力しますので、今回は手動でHTTPリクエストを入れることにします。次のようになります。
$ openssl s_client -CAfile ca.crt -sess_out tls.sess -quiet
depth=1 CN = Six Gates Test CA, O = Soukai Synd., ST = Neo-Saitama, C = JP
verify return:1
depth=0 CN = dhtest.ns, O = Omura Industries MC., ST = Neo-Saitama, C = JP
verify return:1
GET /ns.txt HTTP/1.0
HTTP/1.0 200 ok
Content-type: text/plain
Wasshoi!
read:errno=0
サーバ側がデフォルトのポートで待ち受けしているので、s_client側でもポートの指定は特に必要ありません。しかしwgetの時と同じく、CA証明書を指定しないと接続は切れませんが証明書不正になります。s_clientの場合は-CAfile
というオプションが該当します。なお、コモンネームとドメイン名の対応を自動でチェックはしないので、/etc/hosts
ファイルを書き換える必要はありません。
入出力内容としてはGET
で始まる行が入力したHTTPリクエスト、HTTP
で始まる行から最後のread:
の直前までがサーバからのHTTP応答、それ以外はs_clientの付加情報です。
ひとつ特別に追加しているのは-sess_out
オプションです。これにより、セッション情報をファイルに保存するようにしています。これが後で解析をする際に必要な情報です。
シミュレートに関する参考
ところで、なぜcurlじゃなくてwgetなんだ? と思われる方もいるかもしれません。実は、たまたまwgetを選んだわけではなく、最初curlで試そうと思ったのですがSSL/TLS通信が上手く行かず断念したのです。
おそらく理由としては、wgetの使っているライブラリがOpenSSLのlibssl.soであるのに対し、curlの使っているライブラリがGNUTLSのlibgnutls.soであるという違いがあり、かつGNUTLSではstatic-DHをサポートしていないためではないかと推測しています。調べてはいませんが、FireFoxで使われているNSSライブラリでもstatic-DHはサポートされていないんではないかと思います。
さて、今回のサーバ・クライアント側を一括で実行する ( そして後始末する ) スクリプトも参考に載せておきます。
#!/bin/bash
BIN=openssl
$BIN s_server -cert dh.crt -key dh.key -WWW -quiet >/dev/null 2>/dev/null &
exec 3>&1
coproc clientjob { $BIN s_client -CAfile ca.crt -sess_out tls.sess -quiet >&3; }
exec 3>&-
echo "GET /ns.txt HTTP/1.0" >&${clientjob[1]}
wait %2
kill %1
wait 2>/dev/null
キャプチャと解析
RawCapによる解析
さてでは、通信をキャプチャして解析するわけですが。テスト環境で挙げたWiresharkは両方行えるのですが、実はWindowsの場合ホスト上 ( いわゆるループバックインターフェース ) で完結する通信は、そのままではキャプチャできません。
※Linuxであればtcpdump -s 0 -i lo -w FILE tcp port 4433
なんかで良いんですが。
そこで ( 幾つか方法はあるようなのですが ) RawCapで通信データをダンプファイルに保存し、それをWiresharkで読み込んで解析するという2段構えを採ります。
RawCapを使用するには、単にExplorer上でアイコンをダブルクリックするだけです。ただしUACによる特権使用の確認ダイアログが出ます。
後は、キャプチャするインターフェースとしてLoobackに対応する数値と、ダンプ先のファイル名を入力します。
キャプチャが走り出すと次のように、パケット数がカウントアップされていきますので、上のテスト通信を走らせます。終わったらCtrl-Cで止めます。
なお、RawCapには色々コマンドラインオプションがあって、テキトーなオプションでヘルプも出せるのですが、普通にコマンドプロンプトで実行すると別画面が一瞬出てすぐに消えてしまいます。予め管理者としてコマンドプロンプトを起動して、そこでコマンド入力すればちゃんとヘルプ等メッセージが残ります。
Wiresharkによる解析
続いて、保存されたダンプファイルを開きます。拡張子.pcap
への関連付けができていれば、ファイルアイコンをダブルクリックするだけですが、できていない場合はWiresharkを起動してから、File
→Open
です。
で、テスト通信以外にも様々な内部通信が混じっている可能性があるので、まずはtcp.port==4433
をfilter欄 ( メニューやアイコンのすぐ下 ) に入力してフィルタをかけてみるわけですが…さっぱりアプリケーションレベルの情報が出てきません。
それもそのはずで、TCP4433番をWiresharkはデフォルトでSSL/TLS通信と見做してないからです。なので、Decode As
メニューから設定を追加してあげます。Analyze
メニューからでも良いのですが、適当なパケットを選んで右クリックから辿る方が分かり易いでしょう。
なお、この設定はWiresharkを終了しても残り続けるので、不要になったらマイナスのボタンで消しておくと良いと思います。
そうすることでSSL/TLS通信を認識するようになりますから、TCPレベルのSYNとかを除くため、フィルタに&& ssl
を追加しておきます。
いかにもSSL/TLSな遣り取りが現れるわけですが、このままだと肝心の通信の中身は暗号化されたまま見えません。
そこで今回はマスターシークレットログファイルを使って、ここを暴いていきます。
その準備として、まずはClient Hello
のパケットのRandomという項目 ( 中ほどのペインから選択 ) を右クリックし、Copy
→as a Hex Stream
で、クライアントランダムの値の16進文字列をコピーし、エディタか何かに貼り付けておきます。
続いて、通信時に生成された ( s_clientの-sess_out
オプションで指定した ) tls.sess
ファイルからマスターシークレットの値を引っ張り出します。
これは、opensslのsess_idサブコマンドを使います。そのままMaster-Key
となっている、これまた16進文字列を控えます。
$ openssl sess_id -in tls.sess -text -noout
Protocol : TLSv1.2
Cipher : 00A1
Session-ID: 363A489C2E7221D26AFC6D7F943CE828E4D43240F8FB2975C6BB495EF3AE53BC
Session-ID-ctx:
Master-Key: 1015D45D237A3DBAEF7CC7435D1C82ADBC710513D5CAD0E59D9B43DD73C14DCCE020BD0152BC7328CED697CF41C93365
Key-Arg : None
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 300 (seconds)
TLS session ticket:
0000 - 8d d5 a5 3e c1 ae 92 bb-6b 52 2b 9d 44 38 c4 0d ...>....kR+.D8..
0010 - ca fd e7 e9 01 51 dd 8a-28 8d b5 e8 97 96 b3 a7 .....Q..(.......
0020 - 77 9b 0a 0b 6a ce b0 b2-c1 7d 24 4f 1c a0 16 62 w...j....}$O...b
0030 - 53 fe 27 ee be 3f 55 2c-cc f5 72 e9 49 22 19 8a S.'..?U,..r.I"..
0040 - 8c 3a 26 20 31 28 a4 bc-4f 7c 91 97 4d 0c 03 53 .:& 1(..O|..M..S
0050 - ab 2f c5 29 38 87 b6 eb-36 90 05 50 a1 c4 62 ed ./.)8...6..P..b.
0060 - d0 bd 6a f6 34 44 5e 41-5b f9 e7 20 2d df 3e f7 ..j.4D^A[.. -.>.
0070 - 96 64 ca 2f 17 81 35 19-21 ac 5b e3 3f 9d 7b 5e .d./..5.!.[.?.{^
0080 - a9 c3 89 35 1e fc b5 0c-e1 cb d0 88 c7 13 ed cf ...5............
0090 - 89 8d bc a1 62 21 b8 03-15 ea 76 f5 63 2f c5 7f ....b!....v.c/..
Start Time: 1515030731
Timeout : 300 (sec)
Verify return code: 0 (ok)
そして次のように、CLIENT_RANDOM
という文字列と、Wiresharkから取ったクライアントランダム値、マスターシークレットを空白区切りで1行に並べたファイル ( ここではsslkey.log
) を作ります。
CLIENT_RANDOM f183c17c458b42fead75327e3057e052d5ed233683472856f59668ce00fe6347 1015D45D237A3DBAEF7CC7435D1C82ADBC710513D5CAD0E59D9B43DD73C14DCCE020BD0152BC7328CED697CF41C93365
後はそのファイルをWiresharkに設定してあげるだけです。Edit
→Preference
で出るPreferenceウインドウの中で、Protocols
にリストアップされているプロトコル群からSSL
を選び、(Pre)-Master-Secret log filename
欄にファイル名を入力します。
するとこれこの通り。暗号化されて見えなかった部分、具体的にはFinishedメッセージ以降が見えるようになります。実際の通信内容はSSL segment data
が該当します。
キャプチャと解析に関する参考
さて。そもそもの話として、DH-Ephemeralではなく、static-DHであるということだったはずですが、それはどこで分かるかと言うと。次のようにServer Helloで指定されているCipher Suite、今回はTLS_DH_RSA_WITH_AES_256_GCM_SHA384
というところから分かります。
まあこれ自体は、opensslの出力を拾っても分かることではありますが。
注意が必要なのは、ここで出てくるRSAというのは、証明書の署名に使われているのがRSA署名である、ということであって、鍵交換はおろか認証 ( 証明書の持ち主を検証するフェーズ ) でもRSAの計算はされない、ということです。
※ただRSAの名前が入ってくるのは、証明書のCA署名の検証方式もCipherSuiteの範疇になっているからです。
そして、Client Key Exchangeの内容を見るに、確かにDHで鍵交換を行っていることが分かりますが、対になるServer Key Exchangeがありません。これが、証明書に含まれる鍵を固定的に使っていることを示しています。
実際、Ephemeralな方式を使った場合は、次のtwitterとの通信例のように ( DHではなくECDHという違いはありますが ) Server Key Exchangeの中で鍵交換のためのサーバ側公開鍵が現れます。
終わりに
ということで適当にまとめてみます。
- DH/ECDHにはEphemralとstaticの2通りの方式がある
- staticの場合は、DH/ECDHがKx(鍵交換)、Au(認証)を兼ねる
- DHの公開鍵を証明書に含めることでstatic-DHの通信ができる
- opensslのs_server,s_clientで通信のテストができる
- opensslをライブラリとして使うwgetでもstatic-DHの通信が可能。GNUTLSではおそらくできない。
- マスターシークレットが分かっていれば、WiresharkでSSL/TLS通信を解読できる
ところで、DHの公開鍵から証明書を作る、opensslのx509サブコマンドの-force_pubkey
オプションですが、これはopenssl1.0.2で追加されたものの、1.1.0で削除されたらしいです。…まあ、static-DHは今後TLS1.3で無くなる定めですし、儚くもしようがない所ですね。