All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sshtools.common.knownhosts.KnownHostsKeyVerification Maven / Gradle / Ivy

/**
 * (c) 2002-2021 JADAPTIVE Limited. All Rights Reserved.
 *
 * This file is part of the Maverick Synergy Java SSH API.
 *
 * Maverick Synergy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Maverick Synergy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Maverick Synergy.  If not, see .
 */
/* HEADER */
package com.sshtools.common.knownhosts;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.sshtools.common.publickey.OpenSshCertificate;
import com.sshtools.common.publickey.SshKeyUtils;
import com.sshtools.common.ssh.SshException;
import com.sshtools.common.ssh.components.ComponentManager;
import com.sshtools.common.ssh.components.SshHmac;
import com.sshtools.common.ssh.components.SshPublicKey;
import com.sshtools.common.util.Base64;
import com.sshtools.common.util.Utils;

/**
 * 

* An abstract * HostKeyVerification * class implementation providing validation against the known_hosts format. *

* * @author Lee David Painter */ public class KnownHostsKeyVerification implements HostKeyVerification, HostKeyUpdater { LinkedList entries = new LinkedList<>(); Set keyEntries = new LinkedHashSet<>(); Set revokedEntries = new LinkedHashSet<>(); Map> entriesByPublicKey = new HashMap<>(); List certificateAuthorities = new ArrayList<>(); // Hashed support private boolean hashHosts = false; private boolean useCanonicalHostname = System.getProperty("maverick.knownHosts.enableReverseDNS", "true") .equalsIgnoreCase("true"); private boolean useReverseDNS = System.getProperty("maverick.knownHosts.enableReverseDNS", "true") .equalsIgnoreCase("true"); private static final String HASH_MAGIC = "|1|"; private static final String HASH_DELIM = "|"; Pattern nonStandard = Pattern.compile("\\[([^\\]]+)\\]:([\\d]{1,5})"); public KnownHostsKeyVerification(InputStream in) throws SshException, IOException { load(in); } public KnownHostsKeyVerification(String knownhosts) throws SshException, IOException { load(new ByteArrayInputStream(Utils.getUTF8Bytes(knownhosts))); } public KnownHostsKeyVerification() { } public synchronized void clear() { entries.clear(); keyEntries.clear(); revokedEntries.clear(); entriesByPublicKey.clear(); certificateAuthorities.clear(); } public synchronized void load(InputStream in) throws SshException, IOException { clear(); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); String line; try { while ((line = reader.readLine()) != null) { line = line.trim(); if (line.equals("")) { entries.add(new BlankEntry()); continue; } if (line.startsWith("#")) { entries.add(new CommentEntry(line.substring(1))); continue; } StringTokenizer tokens = new StringTokenizer(line, " "); if (!tokens.hasMoreTokens()) { entries.add(new InvalidEntry(line)); try { onInvalidHostEntry(line); } catch (SshException e) { } continue; } String host = (String) tokens.nextElement(); String marker = ""; if (host.startsWith("@")) { marker = host; host = (String) tokens.nextElement(); } String algorithm = null; try { if (!tokens.hasMoreTokens()) { entries.add(new InvalidEntry(line)); try { onInvalidHostEntry(line); } catch (SshException e) { } continue; } algorithm = tokens.nextToken(); if (!loadSsh1PublicKey(host, algorithm, tokens, line)) { if (!tokens.hasMoreTokens()) { entries.add(new InvalidEntry(line)); try { onInvalidHostEntry(line); } catch (SshException e) { } continue; } SshPublicKey key = SshKeyUtils.getPublicKey(algorithm + " " + tokens.nextToken()); StringBuffer comment = new StringBuffer(); while (tokens.hasMoreTokens()) { if (comment.length() > 0) { comment.append(" "); } comment.append(tokens.nextToken()); } loadSsh2PublicKey(host, marker, algorithm, key, comment.toString()); } } catch (IOException e) { entries.add(new InvalidEntry(line)); try { onInvalidHostEntry(line); } catch (SshException e2) { } } catch (SshException e) { entries.add(new InvalidEntry(line)); try { onInvalidHostEntry(line); } catch (SshException e2) { } } catch (OutOfMemoryError ox) { reader.close(); throw new SshException("Error parsing known_hosts file, is your file corrupt?", SshException.POSSIBLE_CORRUPT_FILE); } } } finally { reader.close(); in.close(); } } private Set getNames(String host) { return new LinkedHashSet(Arrays.asList(host.split(","))); } private void loadSsh2PublicKey(String host, String marker, String algorithm, SshPublicKey key, String comment) throws SshException { KeyEntry entry; if (marker.equalsIgnoreCase("@cert-authority")) { CertAuthorityEntry e = new CertAuthorityEntry(getNames(host), key, comment); certificateAuthorities.add(e); entry = e; } else if (marker.equalsIgnoreCase("@revoked")) { entry = new RevokedEntry(getNames(host), new Ssh2KeyEntry(getNames(host), key, comment)); } else { entry = new Ssh2KeyEntry(getNames(host), key, comment); } addEntry(entry); } private void addEntry(KeyEntry entry) { if (!entriesByPublicKey.containsKey(entry.getKey())) { entriesByPublicKey.put(entry.getKey(), new ArrayList()); } entries.add(entry); entriesByPublicKey.get(entry.getKey()).add(entry); if (entry instanceof KeyEntry) { keyEntries.add(entry); } if (entry instanceof RevokedEntry) { revokedEntries.add(entry); } onHostKeyAdded(getNames(entry.getNames()), entry.getKey()); } protected void onHostKeyAdded(Set names, SshPublicKey key) { } public synchronized void setComment(KeyEntry entry, String comment) { if (!keyEntries.contains(entry)) { throw new IllegalArgumentException("KeyEntry provided is no longer in this known_hosts file."); } entry.comment = comment; } private boolean loadSsh1PublicKey(String host, String algorithm, StringTokenizer tokens, String line) throws SshException { if (!algorithm.matches("[0-9]+")) { return false; } if (!tokens.hasMoreTokens()) { // Do not fail just tell the implementation to // allow it to decide what to do. entries.add(new InvalidEntry(line)); try { onInvalidHostEntry(line); } catch (SshException e) { } return true; } @SuppressWarnings("unused") String e = (String) tokens.nextElement(); if (!tokens.hasMoreTokens()) { entries.add(new InvalidEntry(line)); try { onInvalidHostEntry(line); } catch (SshException e2) { } return true; } entries.add(new Ssh1KeyEntry(line)); return true; } public synchronized void setHashHosts(boolean hashHosts) { this.hashHosts = hashHosts; } protected void onInvalidHostEntry(String entry) throws SshException { // Do nothing } /** *

* Called by the verifyHost method when the host key supplied by * the host does not match the current key recording in the known hosts file. *

* * @param host the name of the host * @param allowedHostKey the current key recorded in the known_hosts file. * @param actualHostKey the actual key supplied by the user * * @throws SshException if an error occurs * * @since 0.2.0 */ protected void onHostKeyMismatch(String host, List allowedHostKey, SshPublicKey actualHostKey) throws SshException { } /** *

* Called by the verifyHost method when the host key supplied is * not recorded in the known_hosts file. *

* *

*

* * @param host the name of the host * @param key the public key supplied by the host * * @throws SshException if an error occurs * * @since 0.2.0 */ protected void onUnknownHost(String host, SshPublicKey key) throws SshException { } /** * Called by the verifyHost method when the host key supplied is * listed as a revoked key. This is informational, any changes made to the * current entries will still result in a failed host verification. * * @param host * @param key * @throws SshException */ protected void onRevokedKey(String host, SshPublicKey key) { } /** *

* Removes an allowed host. *

* * @param host the host to remove * @throws SshException * * @since 0.2.0 */ public synchronized void removeEntries(String host) throws SshException { List toRemove = new ArrayList<>(); for (KeyEntry entry : getKeyEntries()) { if (entry.matchesHost(host)) { toRemove.add(entry); } } removeEntry(toRemove.toArray(new KeyEntry[0])); } public synchronized void removeEntries(String... hosts) throws SshException { for (String host : hosts) { removeEntries(host); } } public synchronized void removeEntries(SshPublicKey key) { List toRemove = entriesByPublicKey.get(key); removeEntry(toRemove.toArray(new KeyEntry[0])); } public synchronized void removeEntry(KeyEntry... keys) { List toRemove = Arrays.asList(keys); keyEntries.removeAll(toRemove); revokedEntries.removeAll(toRemove); entries.removeAll(toRemove); for (Map.Entry> entry : entriesByPublicKey.entrySet()) { entry.getValue().removeAll(toRemove); } certificateAuthorities.removeAll(toRemove); for (KeyEntry entry : keys) { onHostKeyRemoved(getNames(entry.getNames()), entry.getKey()); } } protected void onHostKeyRemoved(Set names, SshPublicKey key) { } public boolean isHostFileWriteable() { return true; } public void allowHost(String host, SshPublicKey key, boolean always) throws SshException { addEntry(key, "", resolveNames(host).toArray(new String[0])); } public synchronized void addEntry(SshPublicKey key, String comment, String... names) throws SshException { if (useHashHosts()) { for (String name : names) { addEntry(new Ssh2KeyEntry(new HashSet(Arrays.asList(generateHash(name))), key, comment)); } } else { addEntry(new Ssh2KeyEntry(new HashSet(Arrays.asList(names)), key, comment)); } } /** *

* Verifies a host key against the list of known_hosts. *

* *

* If the host unknown or the key does not match the currently allowed host key * the abstract onUnknownHost or onHostKeyMismatch * methods are called so that the caller may identify and allow the host. *

* * @param host the name of the host * @param pk the host key supplied * * @return true if the host is accepted, otherwise false * * @throws SshException if an error occurs * * @since 0.2.0 */ public synchronized boolean verifyHost(String host, SshPublicKey pk) throws SshException { return verifyHost(host, pk, true); } private synchronized boolean verifyHost(String host, SshPublicKey pk, boolean validateUnknown) throws SshException { Set resolvedNames = resolveNames(host); for (KeyEntry entry : revokedEntries) { if (entry.validate(pk, resolvedNames.toArray(new String[0]))) { onRevokedKey(host, pk); return false; } } if (entriesByPublicKey.containsKey(pk)) { List keys = entriesByPublicKey.get(pk); for (KeyEntry entry : keys) { if (entry.validate(pk, resolvedNames.toArray(new String[0]))) { return true; } } } if (pk instanceof OpenSshCertificate) { for (CertAuthorityEntry ca : certificateAuthorities) { if (ca.validate(pk, resolvedNames.toArray(new String[0]))) { return true; } } } // The host is unknown os ask the user if (!validateUnknown) return false; onUnknownHost(host, pk); // Recheck ans return the result return verifyHost(host, pk, false); } protected Set resolveNames(String host) { String fqn = null; String ip = null; String resolveHost = host; Set resolvedNames = new LinkedHashSet(); resolvedNames.add(host); Matcher m = nonStandard.matcher(host); boolean nonStandardPorts = m.matches(); if (nonStandardPorts) { resolveHost = m.group(1); } if (useCanonicalHostname() || useReverseDNS()) { try { InetAddress addr = InetAddress.getByName(resolveHost); if (useCanonicalHostname()) { if (nonStandardPorts) { fqn = String.format("[%s]:%s", addr.getHostName(), m.group(2)); } else { fqn = addr.getHostName(); } resolvedNames.add(fqn); } if (useReverseDNS()) { if (nonStandardPorts) { ip = String.format("[%s]:%s", addr.getHostAddress(), m.group(2)); } else { ip = addr.getHostAddress(); } resolvedNames.add(ip); } } catch (UnknownHostException ex) { // Just record the host as the user typed it } } return resolvedNames; } public boolean useCanonicalHostname() { return useCanonicalHostname; } public boolean useReverseDNS() { return useReverseDNS; } public boolean useHashHosts() { return hashHosts; } private boolean checkHash(String name, String resolvedName) throws SshException { SshHmac sha1 = (SshHmac) ComponentManager.getInstance().supportedHMacsCS().getInstance("hmac-sha1"); String hashData = name.substring(HASH_MAGIC.length()); String hashSalt = hashData.substring(0, hashData.indexOf(HASH_DELIM)); String hashStr = hashData.substring(hashData.indexOf(HASH_DELIM) + 1); byte[] theHash = Base64.decode(hashStr); sha1.init(Base64.decode(hashSalt)); sha1.update(resolvedName.getBytes()); byte[] ourHash = sha1.doFinal(); return Arrays.equals(theHash, ourHash); } private String generateHash(String host) throws SshException { SshHmac sha1 = (SshHmac) ComponentManager.getInstance().supportedHMacsCS().getInstance("hmac-sha1"); byte[] hashSalt = new byte[sha1.getMacLength()]; ComponentManager.getInstance().getRND().nextBytes(hashSalt); sha1.init(hashSalt); sha1.update(host.getBytes()); byte[] theHash = sha1.doFinal(); return HASH_MAGIC + Base64.encodeBytes(hashSalt, false) + HASH_DELIM + Base64.encodeBytes(theHash, false); } /** *

* Outputs the allowed hosts in the known_hosts file format. *

* *

* The format consists of any number of lines each representing one key for a * single host. *

* titan,192.168.1.12 ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4Ed..... * titan,192.168.1.12 ssh-rsa AAAAB3NzaC1kc3MAAACBAP1/U4Ed..... * einstein,192.168.1.40 ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4Ed..... * * @return String * * @since 0.2.0 */ public synchronized String toString() { StringBuffer buf = new StringBuffer(""); for (HostFileEntry entry : entries) { buf.append(entry.getFormattedLine()); buf.append(System.getProperty("line.separator")); } return buf.toString(); } public abstract class HostFileEntry { abstract String getFormattedLine(); abstract boolean canValidate(); abstract boolean validate(SshPublicKey key, String... resolvedNames) throws SshException; } public abstract class KeyEntry extends HostFileEntry { String comment; Set names; SshPublicKey key; boolean hashedEntry = false; KeyEntry(Set names, SshPublicKey key, String comment) { this.names = names; this.key = key; this.comment = comment; if (names.size() == 1) { if (names.iterator().next().startsWith(HASH_DELIM)) { hashedEntry = true; } } } public boolean isHashedEntry() { return hashedEntry; } public SshPublicKey getKey() { return key; } public String getNames() { StringBuffer buf = new StringBuffer(); for (String name : names) { if (buf.length() > 0) { buf.append(","); } buf.append(name); } return buf.toString(); } boolean matchesHash(String name, String... resolvedNames) throws SshException { for (String resolvedName : resolvedNames) { if (checkHash(name, resolvedName)) { return true; } } return false; } boolean matchesHost(String... resolvedNames) throws SshException { boolean success = true; boolean matched = false; for (String name : names) { if (name.startsWith(HASH_MAGIC)) { return matchesHash(name, resolvedNames); } else { if (name.startsWith("!")) { if (matches(name.substring(1), resolvedNames)) { success = false; matched = true; } } else { if (matches(name, resolvedNames)) { matched = true; } } } } if (matched) { return success; } return false; } @Override boolean canValidate() { return true; } @Override boolean validate(SshPublicKey key, String... resolvedNames) throws SshException { if (matchesHost(resolvedNames)) { return key.equals(this.key); } return false; } boolean matches(String name, String... resolvedNames) { // First escape any dots name = name.replace(".", "\\."); name = name.replace("[", "\\["); name = name.replace("]", "\\]"); if (name.contains("*")) { name = name.replace("*", ".*"); } if (name.contains("?")) { name = name.replace("?", "."); } for (String resolvedName : resolvedNames) { if (resolvedName.matches(name)) { return true; } } return false; } public String getComment() { return comment; } public boolean isRevoked() { return false; } public boolean isCertAuthority() { return false; } } class Ssh1KeyEntry extends HostFileEntry { String line; Ssh1KeyEntry(String line) { this.line = line; } @Override String getFormattedLine() { return line; } @Override boolean canValidate() { return false; } @Override boolean validate(SshPublicKey key, String... resolvedNames) throws SshException { return false; } } public class Ssh2KeyEntry extends KeyEntry { boolean hashedEntry = false; Ssh2KeyEntry(Set names, SshPublicKey key, String comment) { super(names, key, comment); if (names.size() == 1) { if (names.iterator().next().startsWith(HASH_DELIM)) { hashedEntry = true; } } } public boolean isHashedEntry() { return hashedEntry; } @Override String getFormattedLine() { try { return String.format("%s %s", getNames(), SshKeyUtils.getFormattedKey(key, comment)).trim(); } catch (IOException e) { throw new IllegalStateException(e.getMessage(), e); } } } public class CertAuthorityEntry extends KeyEntry { CertAuthorityEntry(Set names, SshPublicKey key, String comment) { super(names, key, comment); } @Override String getFormattedLine() { try { return String.format("@cert-authority %s %s", getNames(), SshKeyUtils.getFormattedKey(key, comment)); } catch (IOException e) { throw new IllegalStateException(e.getMessage(), e); } } @Override boolean canValidate() { return true; } @Override boolean validate(SshPublicKey key, String... resolvedNames) throws SshException { if (matchesHost(resolvedNames)) { if (key instanceof OpenSshCertificate) { return ((OpenSshCertificate) key).getSignedBy().equals(this.key); } } return false; } @Override public final boolean isCertAuthority() { return true; } } public class RevokedEntry extends KeyEntry { KeyEntry revokedEntry; RevokedEntry(Set names, KeyEntry revokedEntry) { super(names, revokedEntry.getKey(), revokedEntry.getComment()); this.revokedEntry = revokedEntry; } @Override String getFormattedLine() { return String.format("@revoked %s", revokedEntry.getFormattedLine()); } @Override boolean canValidate() { return true; } @Override public final boolean isRevoked() { return true; } } public class CommentEntry extends NonValidatingFileEntry { String comment; CommentEntry(String comment) { this.comment = comment; } @Override String getFormattedLine() { return String.format("#%s", comment); } } public class InvalidEntry extends NonValidatingFileEntry { String line; InvalidEntry(String line) { this.line = line; } @Override String getFormattedLine() { return line; } } public class BlankEntry extends NonValidatingFileEntry { @Override String getFormattedLine() { return ""; } } abstract class NonValidatingFileEntry extends HostFileEntry { @Override boolean canValidate() { return false; } @Override boolean validate(SshPublicKey key, String... resolvedNames) throws SshException { throw new UnsupportedOperationException(); } } public void setUseCanonicalHostnames(boolean value) { this.useCanonicalHostname = value; } public void setUseReverseDNS(boolean value) { this.useReverseDNS = value; } public Set getKeyEntries() { return keyEntries; } @Override public boolean isKnownHost(String host, SshPublicKey key) throws SshException { return verifyHost(host, key, false); } @Override public void updateHostKey(String host, SshPublicKey key) throws SshException { KeyEntry existingEntry = null; Set names = resolveNames(host); for (KeyEntry e : getKeyEntries()) { if (e.isHashedEntry()) { if (e.matchesHash(e.getNames(), names.toArray(new String[0]))) { existingEntry = e; } } else if (e.matchesHost(names.toArray(new String[0]))) { existingEntry = e; } } if (existingEntry != null) { removeEntries(host); } addEntry(key, "", names.toArray(new String[0])); if (existingEntry != null) { onHostKeyUpdated(names, key); } } protected void onHostKeyUpdated(Set names, SshPublicKey key) { } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy