🏢

~/.ssh/known_hostsのホスト名

3 min read

SSHのホスト鍵不整合の検証をするために ~/.ssh/known_hosts をいじろうと見たら、ホスト名がそのまま入らない形になっていたので、どういう形式なのか調べてみた。

調査

ssh(1)のFILESセクションの ~/.ssh/known_hosts の所に "See sshd(8) for further details of the format of this file." とあったのでsshd(8)を見ると、"Alternately, hostnames may be stored in a hashed form which hides host names and addresses should the file's contents be disclosed." とのこと。

openssh-portable/sshd.8 を git blame すると、この説明が入ったコミットは e1776155d19db4f3ab2ff42323d6499f0712cfa4 だった。

この変更で追加されたhost_hash()関数の実装を見ると、ホスト名の表記は

|1|uu_salt|uu_result

の形式。ランダムに生成したsaltを暗号鍵、ホスト名またはIPアドレスをメッセージ本文としてHMAC-SHA1を計算しresultを求める。saltとresultそれぞれをbase64エンコードしたものがuu_salt, uu_resultの部分に入る。

検証

試しに手元で ssh 127.0.0.1 して、~/.ssh/known_hostsに追加された行のホスト名の部分を見ると

|1|w5rs6zNjRawpzwvUObZ504xTtBI=|mgoOHosse3vsmG50zzJ7tIf6n5c=

だった。Pythonを使ってuu_saltと'127.0.0.1'からuu_resultを計算すると、

>>> import base64
>>> salt = base64.b64decode('w5rs6zNjRawpzwvUObZ504xTtBI=')
>>> salt
b'\xc3\x9a\xec\xeb3cE\xac)\xcf\x0b\xd49\xb6y\xd3\x8cS\xb4\x12'
>>> import hmac
>>> import hashlib
>>> m = hmac.new(salt, b'', hashlib.sha1)
>>> m.update(b'127.0.0.1')
>>> base64.b64encode(m.digest())
b'mgoOHosse3vsmG50zzJ7tIf6n5c='

確かに~/.ssh/known_hosts に記載されていたuu_resultと一致している。

>>> base64.b64encode(hmac.digest(salt, b'127.0.0.1', hashlib.sha1))
b'mgoOHosse3vsmG50zzJ7tIf6n5c='

でも良い。

補足

IPアドレスではなくホスト名指定で接続した場合には、known_hostsにはホスト名とIPアドレスのそれぞれで2行が登録される。ただし、CheckHostIPオプションが有効(デフォルト)であり、かつローカルアドレスじゃなく、ProxyCommandオプションが指定されていない場合。

また、ポート番号がデフォルトの22以外であれば、HMACのメッセージ本文は [ホスト名またはIPアドレス]:ポート番号 の形で入る。(put_host_port()関数)

ツール

~/.ssh/known_hostsを1行ずつ確かめていくのは大変なので、指定したホストに該当する行を取り出すスクリプトを書いた: get_known_host_entry.py

以下のように実行すれば、ホスト名に該当する行を抽出して出力する。

$ get_known_host_entry.py example.com

名前解決はしてないので、IPアドレスで該当する行も抽出するには

$ get_known_host_entry.py $(dig +short example.com)

ハッシュ化しない設定

HashKnownHosts no と設定するとホスト名をハッシュ化せずknown_hostsに格納してくれる。

~/.ssh/config
Host *
	HashKnownHosts no

この状態でホスト鍵を保存すれば、特定ホストに該当するknown_hostsの行の抽出はHMAC-SHA1の計算をしなくてもできる。