We’ve all learned a few years ago that the MD5 hash function is no longer secure in many scenarios. Specifically, we should not be using MD5 to verify data, because an attacker may be able to construct two pieces of data with the same hash and feed us one when we were expecting the other.

Fortunately, the good people of OpenSSH have since come to realise that they, too, should not be using MD5 to verify SSH fingerprints. They have since switched from hex-encoded MD5 fingerprints to base64-encoded SHA256 fingerprints1. Cryptographically speaking, this is a great step forward. Unfortunately, this creates some problems when manually verifying SSH fingerprints of a server. This post explains how to generate ‘new’ fingerprints on a server with an ‘old’ installation of OpenSSH. You can then use this new fingerprint to verify the connection to the server from a client that uses new fingerprints.

TL;DR

If you want to generate a ‘new’ SSH fingerprint (base-64 encoding of SHA256 hash) on an ‘old’ server (which uses MD5 by default), type:

awk '{print $2}' /etc/ssh/ssh_host_ecdsa_key.pub | base64 -d | sha256sum | awk '{print $1}' | xxd -r -p | base64 | rev | cut -c 2- | rev

Why verify?

First and foremost, I am afraid many sysadmins do not verify their connections to SSH servers at all. When connecting to an SSH server to which they have never before connected, they will just click ‘yes’ when asked whether the fingerprint is valid. Given how much attention is given these days to security, this is a shame. An attacker who acts as a man-in-the-middle can intercept the connection attempt and replace the public key with his own, yielding a different fingerprint. When the client accepts this different fingerprint, the attacker can intercept and modify all subsequent communications between server and client. In particular, he can intercept the username and password that the user provides.

The classic way to verify a server is as follows. Imagine you have a server to which you have both SSH and console access. The console access may be through the web interface of your cloud provider.

  1. Login to the server via the console.
  2. Simultaneously, connect to the server via SSH. A popup will appear, asking you to verify the SSH fingerprint.
  3. Note the type of key the server offers (RSA, DSA, ECDSA, etc.).
  4. In the console, type ssh-keygen -lf /etc/ssh/ssh_host_<key type>_key.pub.
  5. Compare the output to the fingerprint that is shown in the popup. If the two match, click yes. If they do not, click no.

New fingerprints

If you use this traditional method with a ‘new’ client and an ‘old’ server, it will fail. The new client will show a fingerprint like SHA256:lE6o0OqGBRRRXbdQ8m1kOowm78p5xnSFwmWEuManw9s. The server, meanwhile, will show something like 5e:52:30:9a:6f:00:ac:f6:46:a1:ab:d5:c0:76:9a:be. You cannot compare these two.

A lot of bad solutions to this problem float around on the internet. The most common one is to instruct the client to keep on using MD52, negating the security benefits of verifying the SHA256 fingerprint. No-one was explaining how to do proper verification if your server OpenSSH version is lagging behind your client version.

The way to do it properly is very similar to the classic method:

  1. Login to the server via the console.
  2. Simultaneously, connect to the server via SSH. A popup will appear, asking you to verify the ‘new’ SSH fingerprint.
  3. Note the type of key the server offers (RSA, DSA, ECDSA, etc.).
  4. In the console, type awk '{print $2}' /etc/ssh/ssh_host_<key type>_key.pub | base64 -d | sha256sum | awk '{print $1}' | xxd -r -p | base64 | rev | cut -c 2- | rev.
  5. Compare the output to the fingerprint that is shown in the popup. If the two match, click yes. If they do not, click no.

How does the command work?

The fingerprint that the server offers is the base64-encoded SHA256 hash of the public key. The command performs this calculation:

  1. awk '{print $2}' /etc/ssh/ssh_host_<key type>_key.pub: Fetch the public key, which is the second field in this file.
  2. base64 -d: Decode the (base64-encoded) public key value into binary.
  3. sha256sum: Calculate the hash of the public key.
  4. awk '{print $1}': Select the first value (the hex representation of the actual hash).
  5. xxd -r -p: Decode the (hex-encoded) hash value into binary.
  6. base64: Encode the binary hash value into base64.
  7. rev: Reverse the hash value (so we can cut the last base64 digit).
  8. cut -c 2-: Cut the last base64 digit of the hash (which is temporarily the first digit).
  9. rev: Re-reverse the hash value.

The reason the last base64 digit is stripped by OpenSSH is that a SHA256 hash contains only 256 bits. Since each base64 digit encodes six bits (\(2^6 = 64\)), only 43 base64 digits are necessary to encode the entire SHA256 hash (\(\lceil 256/6 \rceil = 43\)). The 44th digit would always be superfluous. However, base64 encoding normally uses blocks of four digits to match up nicely with 8-bit representations such as ASCII (four base64 digits equals three ASCII characters).

Footnotes

  1. The change was introduced in OpenSSH 6.8: http://www.openssh.com/txt/release-6.8.

  2. Suggested here and here.