SSH Host Key Validation with JSch Library

Grzegorz Żur

Why should I validate SSH host keys?

If you do not validate host keys, then you are susceptible to man-in-the-middle attack. If you are also using passwords to authenticate, this lets attacker reveal them.

Basic code

Let us start with a basic code example which does not validate host keys.

JSch jSch = new JSch();
try {
    Session session = jSch.getSession(username, host);
    try {
        session.setPassword(password);
        session.connect();
        System.out.println("connected");
    } finally {
        session.disconnect();
    }
} catch (JSchException e) {
    e.printStackTrace();
}

Executing this method results in the following exception.

com.jcraft.jsch.JSchException: UnknownHostKey: example.org…

The JSch library tried to connect to example.org host. It received the key that it did not recognize and raised an exception. You might noticed that similar message appears on the first connection to the SSH server when using OpenSSH.

Adding key

OpenSSH stores known host keys in the ~/.ssh/known_hosts file. Let us use the key from the file. We will associate it with the host name.

JSch jSch = new JSch();
try {
    byte[] key = Base64.getDecoder().decode(encKey);
    HostKey hostKey = new HostKey(host, key);
    jSch.getHostKeyRepository().add(hostKey, null);
    Session session = jSch.getSession(username, host);
    try {
        session.setPassword(password);
        session.connect();
        System.out.println("connected");
    } finally {
        session.disconnect();
    }
} catch (JSchException e) {
    e.printStackTrace();
}

We execute the code again and then we still get the same exception.

com.jcraft.jsch.JSchException: UnknownHostKey: example.org…

It happened because SSH server can have more than one key. It stores one key per encryption algorithm.

All keys

Let us take all the keys from the server. To get them, use ssh-keyscan command from OpenSSH package.

$ ssh-keyscan example.org
# example.org:22 SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2
example.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlz…
# example.org:22 SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2
example.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCr6rd30…
# example.org:22 SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2
example.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHXzudbC…

Now we can associate all of them with the host name.

JSch jSch = new JSch();
try {
    for (String encKey : encKeys) {
        byte[] key = Base64.getDecoder().decode(encKey);
        HostKey hostKey = new HostKey(host, key);
        jSch.getHostKeyRepository().add(hostKey, null);
    }
    Session session = jSch.getSession(username, host);
    try {
        session.setPassword(password);
        session.connect();
        System.out.println("connected");
    } finally {
        session.disconnect();
    }
} catch (JSchException e) {
    e.printStackTrace();
}

Unfortunately we get an exception again.

com.jcraft.jsch.JSchException: invalid key type…

It seems that the SSH server uses the key types that are not recognized by JSch library.

Future proof

It is common for the SSH client and server to implement different encryption algorithms. To overcome the problem client and server negotiates the algorithm they will use. We need to prepare for the changes on the server and in the next versions of JSch library.

The following code will try adding host keys and gracefully handle the key types it no longer or not yet accepts.

JSch jSch = new JSch();
try {
    for (String encKey : encKeys) {
        byte[] key = Base64.getDecoder().decode(encKey);
        try {
            HostKey hostKey = new HostKey(host, key);
            jSch.getHostKeyRepository().add(hostKey, null);
        } catch (JSchException e) {
            // invalid key type
        }
    }
    Session session = jSch.getSession(username, host);
    try {
        session.setPassword(password);
        session.connect();
        System.out.println("connected");
    } finally {
        session.disconnect();
    }
} catch (JSchException e) {
    e.printStackTrace();
}

References

Source code

HostKeys.java

import com.jcraft.jsch.HostKey;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

import java.util.Arrays;
import java.util.Base64;

public class HostKeys {

    public static void main(String[] args) {
        String host = args[0];
        String username = args[1];
        String password = args[2];
        String[] keys = Arrays.copyOfRange(args, 3, args.length);
        HostKeys hostKeys = new HostKeys();
        hostKeys.try1(host, username, password);
        hostKeys.try2(host, username, password, keys[0]);
        hostKeys.try3(host, username, password, keys);
        hostKeys.try4(host, username, password, keys);
    }

    void try1(String host, String username, String password) {
        JSch jSch = new JSch();
        try {
            Session session = jSch.getSession(username, host);
            try {
                session.setPassword(password);
                session.connect();
                System.out.println("connected");
            } finally {
                session.disconnect();
            }
        } catch (JSchException e) {
            e.printStackTrace();
        }
    }

    void try2(String host, String username, String password, String encKey) {
        JSch jSch = new JSch();
        try {
            byte[] key = Base64.getDecoder().decode(encKey);
            HostKey hostKey = new HostKey(host, key);
            jSch.getHostKeyRepository().add(hostKey, null);
            Session session = jSch.getSession(username, host);
            try {
                session.setPassword(password);
                session.connect();
                System.out.println("connected");
            } finally {
                session.disconnect();
            }
        } catch (JSchException e) {
            e.printStackTrace();
        }
    }

    void try3(String host, String username, String password, String[] encKeys) {
        JSch jSch = new JSch();
        try {
            for (String encKey : encKeys) {
                byte[] key = Base64.getDecoder().decode(encKey);
                HostKey hostKey = new HostKey(host, key);
                jSch.getHostKeyRepository().add(hostKey, null);
            }
            Session session = jSch.getSession(username, host);
            try {
                session.setPassword(password);
                session.connect();
                System.out.println("connected");
            } finally {
                session.disconnect();
            }
        } catch (JSchException e) {
            e.printStackTrace();
        }
    }

    void try4(String host, String username, String password, String[] encKeys) {
        JSch jSch = new JSch();
        try {
            for (String encKey : encKeys) {
                byte[] key = Base64.getDecoder().decode(encKey);
                try {
                    HostKey hostKey = new HostKey(host, key);
                    jSch.getHostKeyRepository().add(hostKey, null);
                } catch (JSchException e) {
                    // invalid key type
                }
            }
            Session session = jSch.getSession(username, host);
            try {
                session.setPassword(password);
                session.connect();
                System.out.println("connected");
            } finally {
                session.disconnect();
            }
        } catch (JSchException e) {
            e.printStackTrace();
        }
    }

}