diff options
Diffstat (limited to 'src/main/java/ch/ethz/ssh2/KnownHosts.java')
-rw-r--r-- | src/main/java/ch/ethz/ssh2/KnownHosts.java | 951 |
1 files changed, 951 insertions, 0 deletions
diff --git a/src/main/java/ch/ethz/ssh2/KnownHosts.java b/src/main/java/ch/ethz/ssh2/KnownHosts.java new file mode 100644 index 0000000..3f430bb --- /dev/null +++ b/src/main/java/ch/ethz/ssh2/KnownHosts.java @@ -0,0 +1,951 @@ +/* + * Copyright (c) 2006-2011 Christian Plattner. All rights reserved. + * Please refer to the LICENSE.txt for licensing details. + */ + +package ch.ethz.ssh2; + +import java.io.BufferedReader; +import java.io.CharArrayReader; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.util.LinkedList; +import java.util.List; +import java.util.Vector; + +import ch.ethz.ssh2.crypto.Base64; +import ch.ethz.ssh2.crypto.digest.Digest; +import ch.ethz.ssh2.crypto.digest.HMAC; +import ch.ethz.ssh2.crypto.digest.MD5; +import ch.ethz.ssh2.crypto.digest.SHA1; +import ch.ethz.ssh2.signature.DSAPublicKey; +import ch.ethz.ssh2.signature.DSASHA1Verify; +import ch.ethz.ssh2.signature.RSAPublicKey; +import ch.ethz.ssh2.signature.RSASHA1Verify; +import ch.ethz.ssh2.util.StringEncoder; + +/** + * The <code>KnownHosts</code> class is a handy tool to verify received server hostkeys + * based on the information in <code>known_hosts</code> files (the ones used by OpenSSH). + * <p/> + * It offers basically an in-memory database for known_hosts entries, as well as some + * helper functions. Entries from a <code>known_hosts</code> file can be loaded at construction time. + * It is also possible to add more keys later (e.g., one can parse different + * <code>known_hosts<code> files). + * <p/> + * It is a thread safe implementation, therefore, you need only to instantiate one + * <code>KnownHosts</code> for your whole application. + * + * @author Christian Plattner + * @version $Id: KnownHosts.java 37 2011-05-28 22:31:46Z dkocher@sudo.ch $ + */ + +public class KnownHosts +{ + public static final int HOSTKEY_IS_OK = 0; + public static final int HOSTKEY_IS_NEW = 1; + public static final int HOSTKEY_HAS_CHANGED = 2; + + private class KnownHostsEntry + { + String[] patterns; + Object key; + + KnownHostsEntry(String[] patterns, Object key) + { + this.patterns = patterns; + this.key = key; + } + } + + private final LinkedList<KnownHostsEntry> publicKeys = new LinkedList<KnownHosts.KnownHostsEntry>(); + + public KnownHosts() + { + } + + public KnownHosts(char[] knownHostsData) throws IOException + { + initialize(knownHostsData); + } + + public KnownHosts(String knownHosts) throws IOException + { + initialize(new File(knownHosts)); + } + + public KnownHosts(File knownHosts) throws IOException + { + initialize(knownHosts); + } + + /** + * Adds a single public key entry to the database. Note: this will NOT add the public key + * to any physical file (e.g., "~/.ssh/known_hosts") - use <code>addHostkeyToFile()</code> for that purpose. + * This method is designed to be used in a {@link ServerHostKeyVerifier}. + * + * @param hostnames a list of hostname patterns - at least one most be specified. Check out the + * OpenSSH sshd man page for a description of the pattern matching algorithm. + * @param serverHostKeyAlgorithm as passed to the {@link ServerHostKeyVerifier}. + * @param serverHostKey as passed to the {@link ServerHostKeyVerifier}. + * @throws IOException + */ + public void addHostkey(String hostnames[], String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException + { + if (hostnames == null) + { + throw new IllegalArgumentException("hostnames may not be null"); + } + + if ("ssh-rsa".equals(serverHostKeyAlgorithm)) + { + RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(serverHostKey); + + synchronized (publicKeys) + { + publicKeys.add(new KnownHostsEntry(hostnames, rpk)); + } + } + else if ("ssh-dss".equals(serverHostKeyAlgorithm)) + { + DSAPublicKey dpk = DSASHA1Verify.decodeSSHDSAPublicKey(serverHostKey); + + synchronized (publicKeys) + { + publicKeys.add(new KnownHostsEntry(hostnames, dpk)); + } + } + else + { + throw new IOException("Unknwon host key type (" + serverHostKeyAlgorithm + ")"); + } + } + + /** + * Parses the given known_hosts data and adds entries to the database. + * + * @param knownHostsData + * @throws IOException + */ + public void addHostkeys(char[] knownHostsData) throws IOException + { + initialize(knownHostsData); + } + + /** + * Parses the given known_hosts file and adds entries to the database. + * + * @param knownHosts + * @throws IOException + */ + public void addHostkeys(File knownHosts) throws IOException + { + initialize(knownHosts); + } + + /** + * Generate the hashed representation of the given hostname. Useful for adding entries + * with hashed hostnames to a known_hosts file. (see -H option of OpenSSH key-gen). + * + * @param hostname + * @return the hashed representation, e.g., "|1|cDhrv7zwEUV3k71CEPHnhHZezhA=|Xo+2y6rUXo2OIWRAYhBOIijbJMA=" + */ + public static String createHashedHostname(String hostname) + { + SHA1 sha1 = new SHA1(); + + byte[] salt = new byte[sha1.getDigestLength()]; + + new SecureRandom().nextBytes(salt); + + byte[] hash = hmacSha1Hash(salt, hostname); + + String base64_salt = new String(Base64.encode(salt)); + String base64_hash = new String(Base64.encode(hash)); + + return new String("|1|" + base64_salt + "|" + base64_hash); + } + + private static byte[] hmacSha1Hash(byte[] salt, String hostname) + { + SHA1 sha1 = new SHA1(); + + if (salt.length != sha1.getDigestLength()) + { + throw new IllegalArgumentException("Salt has wrong length (" + salt.length + ")"); + } + + HMAC hmac = new HMAC(sha1, salt, salt.length); + + hmac.update(StringEncoder.GetBytes(hostname)); + + byte[] dig = new byte[hmac.getDigestLength()]; + + hmac.digest(dig); + + return dig; + } + + private boolean checkHashed(String entry, String hostname) + { + if (entry.startsWith("|1|") == false) + { + return false; + } + + int delim_idx = entry.indexOf('|', 3); + + if (delim_idx == -1) + { + return false; + } + + String salt_base64 = entry.substring(3, delim_idx); + String hash_base64 = entry.substring(delim_idx + 1); + + byte[] salt = null; + byte[] hash = null; + + try + { + salt = Base64.decode(salt_base64.toCharArray()); + hash = Base64.decode(hash_base64.toCharArray()); + } + catch (IOException e) + { + return false; + } + + SHA1 sha1 = new SHA1(); + + if (salt.length != sha1.getDigestLength()) + { + return false; + } + + byte[] dig = hmacSha1Hash(salt, hostname); + + for (int i = 0; i < dig.length; i++) + { + if (dig[i] != hash[i]) + { + return false; + } + } + + return true; + } + + private int checkKey(String remoteHostname, Object remoteKey) + { + int result = HOSTKEY_IS_NEW; + + synchronized (publicKeys) + { + for (KnownHostsEntry ke : publicKeys) + { + if (hostnameMatches(ke.patterns, remoteHostname) == false) + { + continue; + } + + boolean res = matchKeys(ke.key, remoteKey); + + if (res == true) + { + return HOSTKEY_IS_OK; + } + + result = HOSTKEY_HAS_CHANGED; + } + } + return result; + } + + private List<Object> getAllKeys(String hostname) + { + List<Object> keys = new Vector<Object>(); + + synchronized (publicKeys) + { + for (KnownHostsEntry ke : publicKeys) + { + if (hostnameMatches(ke.patterns, hostname) == false) + { + continue; + } + + keys.add(ke.key); + } + } + + return keys; + } + + /** + * Try to find the preferred order of hostkey algorithms for the given hostname. + * Based on the type of hostkey that is present in the internal database + * (i.e., either <code>ssh-rsa</code> or <code>ssh-dss</code>) + * an ordered list of hostkey algorithms is returned which can be passed + * to <code>Connection.setServerHostKeyAlgorithms</code>. + * + * @param hostname + * @return <code>null</code> if no key for the given hostname is present or + * there are keys of multiple types present for the given hostname. Otherwise, + * an array with hostkey algorithms is returned (i.e., an array of length 2). + */ + public String[] getPreferredServerHostkeyAlgorithmOrder(String hostname) + { + String[] algos = recommendHostkeyAlgorithms(hostname); + + if (algos != null) + { + return algos; + } + + InetAddress[] ipAdresses = null; + + try + { + ipAdresses = InetAddress.getAllByName(hostname); + } + catch (UnknownHostException e) + { + return null; + } + + for (int i = 0; i < ipAdresses.length; i++) + { + algos = recommendHostkeyAlgorithms(ipAdresses[i].getHostAddress()); + + if (algos != null) + { + return algos; + } + } + + return null; + } + + private boolean hostnameMatches(String[] hostpatterns, String hostname) + { + boolean isMatch = false; + boolean negate = false; + + hostname = hostname.toLowerCase(); + + for (int k = 0; k < hostpatterns.length; k++) + { + if (hostpatterns[k] == null) + { + continue; + } + + String pattern = null; + + /* In contrast to OpenSSH we also allow negated hash entries (as well as hashed + * entries in lines with multiple entries). + */ + + if ((hostpatterns[k].length() > 0) && (hostpatterns[k].charAt(0) == '!')) + { + pattern = hostpatterns[k].substring(1); + negate = true; + } + else + { + pattern = hostpatterns[k]; + negate = false; + } + + /* Optimize, no need to check this entry */ + + if ((isMatch) && (negate == false)) + { + continue; + } + + /* Now compare */ + + if (pattern.charAt(0) == '|') + { + if (checkHashed(pattern, hostname)) + { + if (negate) + { + return false; + } + isMatch = true; + } + } + else + { + pattern = pattern.toLowerCase(); + + if ((pattern.indexOf('?') != -1) || (pattern.indexOf('*') != -1)) + { + if (pseudoRegex(pattern.toCharArray(), 0, hostname.toCharArray(), 0)) + { + if (negate) + { + return false; + } + isMatch = true; + } + } + else if (pattern.compareTo(hostname) == 0) + { + if (negate) + { + return false; + } + isMatch = true; + } + } + } + + return isMatch; + } + + private void initialize(char[] knownHostsData) throws IOException + { + BufferedReader br = new BufferedReader(new CharArrayReader(knownHostsData)); + + while (true) + { + String line = br.readLine(); + + if (line == null) + { + break; + } + + line = line.trim(); + + if (line.startsWith("#")) + { + continue; + } + + String[] arr = line.split(" "); + + if (arr.length >= 3) + { + if ((arr[1].compareTo("ssh-rsa") == 0) || (arr[1].compareTo("ssh-dss") == 0)) + { + String[] hostnames = arr[0].split(","); + + byte[] msg = Base64.decode(arr[2].toCharArray()); + + try + { + addHostkey(hostnames, arr[1], msg); + } + catch (IOException e) + { + continue; + } + } + } + } + } + + private void initialize(File knownHosts) throws IOException + { + char[] buff = new char[512]; + + CharArrayWriter cw = new CharArrayWriter(); + + knownHosts.createNewFile(); + + FileReader fr = new FileReader(knownHosts); + + while (true) + { + int len = fr.read(buff); + if (len < 0) + { + break; + } + cw.write(buff, 0, len); + } + + fr.close(); + + initialize(cw.toCharArray()); + } + + private boolean matchKeys(Object key1, Object key2) + { + if ((key1 instanceof RSAPublicKey) && (key2 instanceof RSAPublicKey)) + { + RSAPublicKey savedRSAKey = (RSAPublicKey) key1; + RSAPublicKey remoteRSAKey = (RSAPublicKey) key2; + + if (savedRSAKey.getE().equals(remoteRSAKey.getE()) == false) + { + return false; + } + + if (savedRSAKey.getN().equals(remoteRSAKey.getN()) == false) + { + return false; + } + + return true; + } + + if ((key1 instanceof DSAPublicKey) && (key2 instanceof DSAPublicKey)) + { + DSAPublicKey savedDSAKey = (DSAPublicKey) key1; + DSAPublicKey remoteDSAKey = (DSAPublicKey) key2; + + if (savedDSAKey.getG().equals(remoteDSAKey.getG()) == false) + { + return false; + } + + if (savedDSAKey.getP().equals(remoteDSAKey.getP()) == false) + { + return false; + } + + if (savedDSAKey.getQ().equals(remoteDSAKey.getQ()) == false) + { + return false; + } + + if (savedDSAKey.getY().equals(remoteDSAKey.getY()) == false) + { + return false; + } + + return true; + } + + return false; + } + + private boolean pseudoRegex(char[] pattern, int i, char[] match, int j) + { + /* This matching logic is equivalent to the one present in OpenSSH 4.1 */ + + while (true) + { + /* Are we at the end of the pattern? */ + + if (pattern.length == i) + { + return (match.length == j); + } + + if (pattern[i] == '*') + { + i++; + + if (pattern.length == i) + { + return true; + } + + if ((pattern[i] != '*') && (pattern[i] != '?')) + { + while (true) + { + if ((pattern[i] == match[j]) && pseudoRegex(pattern, i + 1, match, j + 1)) + { + return true; + } + j++; + if (match.length == j) + { + return false; + } + } + } + + while (true) + { + if (pseudoRegex(pattern, i, match, j)) + { + return true; + } + j++; + if (match.length == j) + { + return false; + } + } + } + + if (match.length == j) + { + return false; + } + + if ((pattern[i] != '?') && (pattern[i] != match[j])) + { + return false; + } + + i++; + j++; + } + } + + private String[] recommendHostkeyAlgorithms(String hostname) + { + String preferredAlgo = null; + + List<Object> keys = getAllKeys(hostname); + + for (Object key : keys) + { + String thisAlgo = null; + + if (key instanceof RSAPublicKey) + { + thisAlgo = "ssh-rsa"; + } + else if (key instanceof DSAPublicKey) + { + thisAlgo = "ssh-dss"; + } + else + { + continue; + } + + if (preferredAlgo != null) + { + /* If we find different key types, then return null */ + + if (preferredAlgo.compareTo(thisAlgo) != 0) + { + return null; + } + } + else + { + preferredAlgo = thisAlgo; + } + } + + /* If we did not find anything that we know of, return null */ + + if (preferredAlgo == null) + { + return null; + } + + /* Now put the preferred algo to the start of the array. + * You may ask yourself why we do it that way - basically, we could just + * return only the preferred algorithm: since we have a saved key of that + * type (sent earlier from the remote host), then that should work out. + * However, imagine that the server is (for whatever reasons) not offering + * that type of hostkey anymore (e.g., "ssh-rsa" was disabled and + * now "ssh-dss" is being used). If we then do not let the server send us + * a fresh key of the new type, then we shoot ourself into the foot: + * the connection cannot be established and hence the user cannot decide + * if he/she wants to accept the new key. + */ + + if (preferredAlgo.equals("ssh-rsa")) + { + return new String[] { "ssh-rsa", "ssh-dss" }; + } + + return new String[] { "ssh-dss", "ssh-rsa" }; + } + + /** + * Checks the internal hostkey database for the given hostkey. + * If no matching key can be found, then the hostname is resolved to an IP address + * and the search is repeated using that IP address. + * + * @param hostname the server's hostname, will be matched with all hostname patterns + * @param serverHostKeyAlgorithm type of hostkey, either <code>ssh-rsa</code> or <code>ssh-dss</code> + * @param serverHostKey the key blob + * @return <ul> + * <li><code>HOSTKEY_IS_OK</code>: the given hostkey matches an entry for the given hostname</li> + * <li><code>HOSTKEY_IS_NEW</code>: no entries found for this hostname and this type of hostkey</li> + * <li><code>HOSTKEY_HAS_CHANGED</code>: hostname is known, but with another key of the same type + * (man-in-the-middle attack?)</li> + * </ul> + * @throws IOException if the supplied key blob cannot be parsed or does not match the given hostkey type. + */ + public int verifyHostkey(String hostname, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException + { + Object remoteKey = null; + + if ("ssh-rsa".equals(serverHostKeyAlgorithm)) + { + remoteKey = RSASHA1Verify.decodeSSHRSAPublicKey(serverHostKey); + } + else if ("ssh-dss".equals(serverHostKeyAlgorithm)) + { + remoteKey = DSASHA1Verify.decodeSSHDSAPublicKey(serverHostKey); + } + else + { + throw new IllegalArgumentException("Unknown hostkey type " + serverHostKeyAlgorithm); + } + + int result = checkKey(hostname, remoteKey); + + if (result == HOSTKEY_IS_OK) + { + return result; + } + + InetAddress[] ipAdresses = null; + + try + { + ipAdresses = InetAddress.getAllByName(hostname); + } + catch (UnknownHostException e) + { + return result; + } + + for (int i = 0; i < ipAdresses.length; i++) + { + int newresult = checkKey(ipAdresses[i].getHostAddress(), remoteKey); + + if (newresult == HOSTKEY_IS_OK) + { + return newresult; + } + + if (newresult == HOSTKEY_HAS_CHANGED) + { + result = HOSTKEY_HAS_CHANGED; + } + } + + return result; + } + + /** + * Adds a single public key entry to the a known_hosts file. + * This method is designed to be used in a {@link ServerHostKeyVerifier}. + * + * @param knownHosts the file where the publickey entry will be appended. + * @param hostnames a list of hostname patterns - at least one most be specified. Check out the + * OpenSSH sshd man page for a description of the pattern matching algorithm. + * @param serverHostKeyAlgorithm as passed to the {@link ServerHostKeyVerifier}. + * @param serverHostKey as passed to the {@link ServerHostKeyVerifier}. + * @throws IOException + */ + public static void addHostkeyToFile(File knownHosts, String[] hostnames, String serverHostKeyAlgorithm, + byte[] serverHostKey) throws IOException + { + if ((hostnames == null) || (hostnames.length == 0)) + { + throw new IllegalArgumentException("Need at least one hostname specification"); + } + + if ((serverHostKeyAlgorithm == null) || (serverHostKey == null)) + { + throw new IllegalArgumentException(); + } + + CharArrayWriter writer = new CharArrayWriter(); + + for (int i = 0; i < hostnames.length; i++) + { + if (i != 0) + { + writer.write(','); + } + writer.write(hostnames[i]); + } + + writer.write(' '); + writer.write(serverHostKeyAlgorithm); + writer.write(' '); + writer.write(Base64.encode(serverHostKey)); + writer.write("\n"); + + char[] entry = writer.toCharArray(); + + RandomAccessFile raf = new RandomAccessFile(knownHosts, "rw"); + + long len = raf.length(); + + if (len > 0) + { + raf.seek(len - 1); + int last = raf.read(); + if (last != '\n') + { + raf.write('\n'); + } + } + + raf.write(StringEncoder.GetBytes(new String(entry))); + raf.close(); + } + + /** + * Generates a "raw" fingerprint of a hostkey. + * + * @param type either "md5" or "sha1" + * @param keyType either "ssh-rsa" or "ssh-dss" + * @param hostkey the hostkey + * @return the raw fingerprint + */ + static private byte[] rawFingerPrint(String type, String keyType, byte[] hostkey) + { + Digest dig = null; + + if ("md5".equals(type)) + { + dig = new MD5(); + } + else if ("sha1".equals(type)) + { + dig = new SHA1(); + } + else + { + throw new IllegalArgumentException("Unknown hash type " + type); + } + + if ("ssh-rsa".equals(keyType)) + { + } + else if ("ssh-dss".equals(keyType)) + { + } + else + { + throw new IllegalArgumentException("Unknown key type " + keyType); + } + + if (hostkey == null) + { + throw new IllegalArgumentException("hostkey is null"); + } + + dig.update(hostkey); + byte[] res = new byte[dig.getDigestLength()]; + dig.digest(res); + return res; + } + + /** + * Convert a raw fingerprint to hex representation (XX:YY:ZZ...). + * + * @param fingerprint raw fingerprint + * @return the hex representation + */ + static private String rawToHexFingerprint(byte[] fingerprint) + { + final char[] alpha = "0123456789abcdef".toCharArray(); + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < fingerprint.length; i++) + { + if (i != 0) + { + sb.append(':'); + } + int b = fingerprint[i] & 0xff; + sb.append(alpha[b >> 4]); + sb.append(alpha[b & 15]); + } + + return sb.toString(); + } + + /** + * Convert a raw fingerprint to bubblebabble representation. + * + * @param raw raw fingerprint + * @return the bubblebabble representation + */ + static private String rawToBubblebabbleFingerprint(byte[] raw) + { + final char[] v = "aeiouy".toCharArray(); + final char[] c = "bcdfghklmnprstvzx".toCharArray(); + + StringBuilder sb = new StringBuilder(); + + int seed = 1; + + int rounds = (raw.length / 2) + 1; + + sb.append('x'); + + for (int i = 0; i < rounds; i++) + { + if (((i + 1) < rounds) || ((raw.length) % 2 != 0)) + { + sb.append(v[(((raw[2 * i] >> 6) & 3) + seed) % 6]); + sb.append(c[(raw[2 * i] >> 2) & 15]); + sb.append(v[((raw[2 * i] & 3) + (seed / 6)) % 6]); + + if ((i + 1) < rounds) + { + sb.append(c[(((raw[(2 * i) + 1])) >> 4) & 15]); + sb.append('-'); + sb.append(c[(((raw[(2 * i) + 1]))) & 15]); + // As long as seed >= 0, seed will be >= 0 afterwards + seed = ((seed * 5) + (((raw[2 * i] & 0xff) * 7) + (raw[(2 * i) + 1] & 0xff))) % 36; + } + } + else + { + sb.append(v[seed % 6]); // seed >= 0, therefore index positive + sb.append('x'); + sb.append(v[seed / 6]); + } + } + + sb.append('x'); + + return sb.toString(); + } + + /** + * Convert a ssh2 key-blob into a human readable hex fingerprint. + * Generated fingerprints are identical to those generated by OpenSSH. + * <p/> + * Example fingerprint: d0:cb:76:19:99:5a:03:fc:73:10:70:93:f2:44:63:47. + * + * @param keytype either "ssh-rsa" or "ssh-dss" + * @param publickey key blob + * @return Hex fingerprint + */ + public static String createHexFingerprint(String keytype, byte[] publickey) + { + byte[] raw = rawFingerPrint("md5", keytype, publickey); + return rawToHexFingerprint(raw); + } + + /** + * Convert a ssh2 key-blob into a human readable bubblebabble fingerprint. + * The used bubblebabble algorithm (taken from OpenSSH) generates fingerprints + * that are easier to remember for humans. + * <p/> + * Example fingerprint: xofoc-bubuz-cazin-zufyl-pivuk-biduk-tacib-pybur-gonar-hotat-lyxux. + * + * @param keytype either "ssh-rsa" or "ssh-dss" + * @param publickey key data + * @return Bubblebabble fingerprint + */ + public static String createBubblebabbleFingerprint(String keytype, byte[] publickey) + { + byte[] raw = rawFingerPrint("sha1", keytype, publickey); + return rawToBubblebabbleFingerprint(raw); + } +} |