[Crypto] SSH protocol dissected, part II: fingerprint

Last time I left my readers with the fact that my toy SSH client is vulnerable to MITM. How do 'real' SSH clients fix this problem?

Fingerprint

When you first connect to a SSH server, you see:

% ssh -oHostkeyAlgorithms=ssh-rsa root@vps5.yurichev.org
The authenticity of host 'vps5.yurichev.org (2a01:4f8:c0c:f902::1)' can't be established.
RSA key fingerprint is SHA256:rM263R9HUl5C/4HhHt1eQRdMh1c73Q/I+XFyzoPD1YI.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

What's with the fingerprint anyway? Let's login to my server and find the /etc/ssh/ssh_host_rsa_key.pub file:

root@vps5:/etc/ssh# cat ssh_host_rsa_key.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCs4LE/+rSAdfO0u0In0vYr/KXdZhza5Qhh7qhkEu8D+FB9Zwc7c4lPVKGu9cfe4H2VPfPvk4AmBDJYqpGeISEhwg6rdNv96n/zdAvlWYB093C76jyInEZFCC4mv+hJocZp67B/YDt0zCiH9xOV55KkjT7RwqKmGJlLEnfD/6hLVdUp8M3TYMcYRpL3+/wwWSFatN9nhiMdGYDIpVWXIj10f5+1Gy36R1op/a4ANCc+SbCYc8RymL2Jd7agGq3hokYAAO3dcQfbWa4GWGLe1SbHmRUuOX4SdUFyBvpNuLi2KZ9AmQFxwhssqeDX5bSOoZyx18LY1T9Qark7xwue5Vsd root@vps5

Let's decode this base64 string and hash it:

root@vps5:/etc/ssh# echo AAAAB3NzaC1yc2EAAAADAQABAAABAQCs4LE/+rSAdfO0u0In0vYr/KXdZhza5Qhh7qhkEu8D+FB9Zwc7c4lPVKGu9cfe4H2VPfPvk4AmBDJYqpGeISEhwg6rdNv96n/zdAvlWYB093C76jyInEZFCC4mv+hJocZp67B/YDt0zCiH9xOV55KkjT7RwqKmGJlLEnfD/6hLVdUp8M3TYMcYRpL3+/wwWSFatN9nhiMdGYDIpVWXIj10f5+1Gy36R1op/a4ANCc+SbCYc8RymL2Jd7agGq3hokYAAO3dcQfbWa4GWGLe1SbHmRUuOX4SdUFyBvpNuLi2KZ9AmQFxwhssqeDX5bSOoZyx18LY1T9Qark7xwue5Vsd | base64 -d | sha256sum

accdbadd1f47525e42ff81e11edd5e41174c87573bdd0fc8f97172ce83c3d582  -

Let's decode base64-encoded fingerprint:

% echo "rM263R9HUl5C/4HhHt1eQRdMh1c73Q/I+XFyzoPD1YI." | base64 -d | xxd -g 1

base64: invalid input
00000000: ac cd ba dd 1f 47 52 5e 42 ff 81 e1 1e dd 5e 41  .....GR^B.....^A
00000010: 17 4c 87 57 3b dd 0f c8 f9 71 72 ce 83 c3 d5 82  .L.W;....qr.....

Same hash. So SSH public key's fingerprint is just a SHA hash of public key encoded by base64. It's shown during first connect.

But keep in mind that these days a SSH server may offer 3-4 public keys: RSA, DSA, ECDSA, ED25519.

How SSH client validate fingerprint

During key exchange, SSH server sends its RSA public key. Also, when a 'hodgepodge' is constructed (the most important part of it is shared secred from DH algo) and hashed (called 'H' value). Server is then sign the 'H' value with its public key and sends signature.

A client, calculating 'hodgepodge' and hash of it ('H'), gets server's public key and check signature. If signature is correct, all is fine. If not, it stops with error.

I added this to my toy SSH client. RSA signature check is so easy I can do it without any 3rd-party libraries.

def chk_kexgex_hash(kexgex_hash):
    if VERBOSITY>=1:
        print ("chk_kexgex_hash()")
    global KEX_H_sig_mpint # 2k bits
    global RSA_e
    global RSA_modulus_n # 2k bits
    # do RSA signature check:
    plaintext_must_be=pow (KEX_H_sig_mpint, RSA_e, RSA_modulus_n)
    # plaintext_must_be:
    # 160 bits if server_host_algorithms="ssh-rsa". SHA1.
    # X bits if server_host_algorithms="rsa-sha2-X". SHA256 or SHA512
    if VERBOSITY>=1:
        print ("plaintext_must_be:", hex(plaintext_must_be))
    # https://stackoverflow.com/questions/7983820/get-the-last-4-characters-of-a-string
    plaintext_must_be_truncated=hex(plaintext_must_be)[-SERVER_HOST_ALGO_NIBBLES:] # last X nibbles/characters for SHA
    if VERBOSITY>=1:
        print ("plaintext_must_be_truncated:", plaintext_must_be_truncated)
    if VERBOSITY>=1:
        print ("kexgex_hash:")
        hexdump.hexdump(kexgex_hash)
    tmp=SERVER_HOST_ALGO_PTR(kexgex_hash).digest()
    if VERBOSITY>=1:
        print (SERVER_HOST_ALGO+"(kexgex_hash):")
        hexdump.hexdump(tmp)
    hash_of_kexgex_hash=SERVER_HOST_ALGO_PTR(kexgex_hash).hexdigest()
    if plaintext_must_be_truncated==hash_of_kexgex_hash:
        if VERBOSITY>=1:
            print ("Server signature is correct")
    else:
        print ("Error. Server signature is incorrect")
        print ("Exiting")
        exit(0)
% ./toyssh_v2.py -v -h vps5.yurichev.org

[...]

server fingerprint: SHA256:rM263R9HUl5C/4HhHt1eQRdMh1c73Q/I+XFyzoPD1YI=
host_key_type: ssh-rsa
RSA_e: 65537
RSA_modulus_n: 21823788397541746167056831174044267366502866938233970429703574867801700806611313702812759153419602287163053092331515213178430695616087101825397177491838911029833856414311843844959542513595618613768321461337682856353266916079826054533744789048384067313814394056726981173334480264778744359881220767511740806265381781452687853243296206274625846750150214442764353935543024113553072293449692087081692987942671958592516689579487288609976886126495858238365051459029546101449948016043344127538488989492145131769630764564859073754134712300234511794682973173979410364124143343680612781553794607782635456774480340812539428690717
binlog(RSA_modulus_n): 2048
server_f: 0x79e7c819ab50ed6857eb1ec3386bfd48978326abac748c67becc6127b1a48343091c79473d1e520857643538afa294381be10df6398347a0ed262c4d2b5db3b1d82ce6db09726d29efd3a77c86ce0c815f59eadcac89f07176e9d8fb6475a7b8c5d3b4b9647aac3f7d41ef332225a2b059dbd7e88d41edcb063d0f37918bcd230babfd9fe6f9a16d35a61a71554625c4d4fb0743418871d58d89ae868da7c3ab321b5af6ebb01f969a0ac26cf48bc4b53072f75c43f27a2905936166bc133765d0d499598db75b4d7dd75a16b356bce9e44c8ff7143406be9070f7fa46f1b9f8089554bf2525b78520dec1228e2859e7b9a90d8994e5261b7fa7b03a41f0fce
unpack_KEX_H_sig
rsa-sha2-256
0x13d8af8442557f9c333cc8686b8589ce106ceceaa230d5e499e69b194825f7fcae89381bafd821c5944f6306836f4b50fd10364095bc5a57064996220c260d836cd3c028256e3aebcd1cb8066efee13addec8688b47f9208bbcaac18435daadee70982498503196687d14bbe24cc0bcdcdaf79eb7e5d17b07466e3ec212ae1e7baedf375bae8ee25d10d1733daebaf09f48545c4689bd9922965f61e0a0a735b9a3cf3e4e959ed1c1c5277e1fc6ea0c1422f9da27967cb065ebac53f5ecc709808c6e9a3895f174ec091d7f76bb0be93f91fe0071b08b745a942d7c04750a5bdb1c508553d6300cc6f6ebd188b9cf73b629703b5e29c1482a8e5f676f1ae02c3
shared_secret: 0xbdf144b0360812d09a957e77acaec1b5647abbc768ea6cc727414d850d2ff3bc05d12e0dc1bf6321633d9dd671f145008d8ea787d578612240331d1c1070859e9b5667b4a7544d0a411df38181dbde55ad418fc7a5288b1d0a5a149eab417186de83c11b6cd14cc6e4135b6d76918101e48fd62ad02c5398dad365d98b26bc572c8555a267ead08ed6b20d9fb332962644a3ed97cf7db23dc38251956fbaf5f1e54714ecd7a063dd8c27b50a1f14cede65575e8590373f12e7fb4f4ddb35689941fc11706cf0dd995e2fa372d4f2068539d5f978a38c0437409babdbc9cac6b928277cc60d1f1e875a3a471f6dffd185e3845e170afcfbedf37b23ca518c7341
chk_kexgex_hash()
plaintext_must_be: 0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d0609608648016503040201050004204335a9d58c4ee2e655dc6fb97b9f8c0ce08896f8a0dcc2ec8d9d314f9b735088
plaintext_must_be_truncated: 4335a9d58c4ee2e655dc6fb97b9f8c0ce08896f8a0dcc2ec8d9d314f9b735088
kexgex_hash:
00000000: DB 57 83 71 79 15 D0 16  ED 46 D6 37 AD 34 02 AB  .W.qy....F.7.4..
00000010: 51 17 0E 8D D4 22 EC 93  AB 83 95 C4 C5 67 DF 9D  Q............g..
rsa-sha2-256(kexgex_hash):
00000000: 43 35 A9 D5 8C 4E E2 E6  55 DC 6F B9 7B 9F 8C 0C  C5...N..U.o.{...
00000010: E0 88 96 F8 A0 DC C2 EC  8D 9D 31 4F 9B 73 50 88  ..........1O.sP.
Server signature is correct

You can ask that a MITM attacker can generate its own RSA key pair and sign 'H' with it. Why not? Yes, he/she can. But his/her fingerprint will be different. The problem of forging fingerprint is of the same hardness as cracking RSA.

So now my toy SSH client is almost secure as a 'real' SSH client. (But don't use it, it's only used for demonstration/learning.)

Fingerprint again

There is a saying: TOFU (Trust on first use). That means that during first connect, you check fingerprint. Then it stored into ~/.ssh/known_hosts.

If, for some reason, during connect, fingerprint is different from that stored in 'known_hosts', you'll see that 'nasty' message:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:P80/MpxGfOFXP5zmz/WiBudWKAofH9v9eXEWrBmJl08.
Please contact your system administrator.
Add correct host key in /home/i/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /home/i/.ssh/known_hosts:34
  remove with:
  ssh-keygen -f "/home/i/.ssh/known_hosts" -R "localhost"
RSA host key for localhost has changed and you have requested strict checking.
Host key verification failed.

Most often you see this when you reinstalled your server (and SSH server as well, so SSH keys would be regenerated), but old fingerprint is still stored in your 'known_hosts' file on client side.

Anyway, if you're paranoid, if you work on something really sensitive, in hostile environment, pay attention to server's fingerprint.

'Random art'

Not strictly a piece of great art:

$ ssh -oVisualHostKey=yes localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ED25519 key fingerprint is SHA256:3/Q8ifMgTDngw0BiyUO5Cjus72AdnJ1DIM7TNycinNE.
+--[ED25519 256]--+
| o.++o.          |
|+ =E*o           |
| B o B...        |
|. + B =+ . .     |
|.o = +  S + .    |
|o.o . .  = + + . |
|oo .      + = =  |
|o.         . + . |
| oo           .  |
+----[SHA256]-----+

This is rather Rorschach inkblot test. But yes, it's widely believed that it's easier to memorize some features of such 'ASCII art' rather than a long base64-encoded string.

Distribution of fingerprint

Sysadmin should distribute SSH server's fingerprint(s) via other channels. Like email. Like website:

The suckless.org project is now hosted on a new server. All inactive accounts have been removed during the relocation.

Please note that the new ECDSA key fingerprint is SHA256:7DBXcYScmsxbv7rMJUJoJsY5peOrngD4QagiXX6MiQU.

( src )

SSH server's fingerprint can be also added to DNS record: SSHFP record

And again, keep in mind that a SSH server may have 3-4 key pairs.

Problems with not checking fingerprint

An issue was discovered in Midnight Commander through 4.8.26. When establishing an SFTP connection, the fingerprint of the server is neither checked nor displayed. As a result, a user connects to the server without the ability to verify its authenticity.

( CVE-2021-36370 )

Obviously, MITM attack can succeed with such a SSH client.

Cloud providers

When you create a virtual server on Hetzner or Linode, they send you IP4/6 and root SSH password. But not SSH fingerprint(s). They install OS (and SSH) for you.

So if you're a real paranoid, you can connect to your virtual server via other channel ("console") and check SSH fingerprints.

On the other hand, it's very easy for cloud provider to read all SSH secret keys from your disk image. Or, in case of dedicated server, reboot it and read all the files from your HDD/SDD.

So yes, cloud providers can mount SSH MITM attack without any problem and fingerprint wouldn't of any help.

(In other words, if an attacker has physical access to your server, you can't be protected even with fingerprint.)

All the files

Second version of my toy SSH client.


Next part.

20240530 22:32:53 EEST: BTW, small factlet. If two SSH servers demonstrate the same fingerprint, this is probably a single SSH server listening on several IP-addresses. I can't say I saw this in wild, but it's theoretically possible.

(the post first published at 20220904.)


List of my other blog posts.

Subscribe to my news feed

Yes, I know about these lousy Disqus ads. Please use adblocker. I would consider to subscribe to 'pro' version of Disqus if the signal/noise ratio in comments would be good enough.