/* * Copyright (C)2009 - SSHJ Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.schmizz.sshj.transport.verification; import net.schmizz.sshj.common.*; import net.schmizz.sshj.transport.mac.HMACSHA1; import net.schmizz.sshj.transport.mac.MAC; import org.slf4j.Logger; import java.io.*; import java.math.BigInteger; import java.security.KeyFactory; import java.security.PublicKey; import java.security.spec.RSAPublicKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * A {@link HostKeyVerifier} implementation for a {@code known_hosts} file i.e. in the format used by OpenSSH. * * @see <a href="http://nms.lcs.mit.edu/projects/ssh/README.hashed-hosts">Hashed hostnames spec</a> */ public class OpenSSHKnownHosts implements HostKeyVerifier { protected final Logger log; protected final File khFile; protected final List<HostEntry> entries = new ArrayList<HostEntry>(); public OpenSSHKnownHosts(File khFile) throws IOException { this(khFile, LoggerFactory.DEFAULT); } public OpenSSHKnownHosts(File khFile, LoggerFactory loggerFactory) throws IOException { this.khFile = khFile; log = loggerFactory.getLogger(getClass()); if (khFile.exists()) { final EntryFactory entryFactory = new EntryFactory(); final BufferedReader br = new BufferedReader(new FileReader(khFile)); try { // Read in the file, storing each line as an entry String line; while ((line = br.readLine()) != null) { try { HostEntry entry = entryFactory.parseEntry(line); if (entry != null) { entries.add(entry); } } catch (SSHException ignore) { log.debug("Bad line ({}): {} ", ignore.toString(), line); } catch (SSHRuntimeException ignore) { log.debug("Failed to process line ({}): {} ", ignore.toString(), line); } } } finally { IOUtils.closeQuietly(br); } } } public File getFile() { return khFile; } @Override public boolean verify(final String hostname, final int port, final PublicKey key) { final KeyType type = KeyType.fromKey(key); if (type == KeyType.UNKNOWN) return false; final String adjustedHostname = (port != 22) ? "[" + hostname + "]:" + port : hostname; for (HostEntry e : entries) { try { if (e.appliesTo(type, adjustedHostname)) return e.verify(key) || hostKeyChangedAction(e, adjustedHostname, key); } catch (IOException ioe) { log.error("Error with {}: {}", e, ioe); return false; } } return hostKeyUnverifiableAction(adjustedHostname, key); } protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) { return false; } protected boolean hostKeyChangedAction(HostEntry entry, String hostname, PublicKey key) { log.warn("Host key for `{}` has changed!", hostname); return false; } public List<HostEntry> entries() { return entries; } private static final String LS = System.getProperty("line.separator"); public void write() throws IOException { final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(khFile)); try { for (HostEntry entry : entries) bos.write((entry.getLine() + LS).getBytes(IOUtils.UTF8)); } finally { bos.close(); } } /** * Append a single entry */ public void write(HostEntry entry) throws IOException { final BufferedWriter writer = new BufferedWriter(new FileWriter(khFile, true)); try { writer.write(entry.getLine()); writer.newLine(); writer.flush(); } finally { IOUtils.closeQuietly(writer); } } public static File detectSSHDir() { final File sshDir = new File(System.getProperty("user.home"), ".ssh"); return sshDir.exists() ? sshDir : null; } /** * Each line in these files contains the following fields: markers * (optional), hostnames, bits, exponent, modulus, comment. The fields are * separated by spaces. * <p/> * The marker is optional, but if it is present then it must be one of * ``@cert-authority'', to indicate that the line contains a certification * authority (CA) key, or ``@revoked'', to indicate that the key contained * on the line is revoked and must not ever be accepted. Only one marker * should be used on a key line. * <p/> * Hostnames is a comma-separated list of patterns (`*' and `?' act as * wildcards); each pattern in turn is matched against the canonical host * name (when authenticating a client) or against the user-supplied name * (when authenticating a server). A pattern may also be preceded by `!' to * indicate negation: if the host name matches a negated pattern, it is not * accepted (by that line) even if it matched another pattern on the line. * A hostname or address may optionally be enclosed within `[' and `]' * brackets then followed by `:' and a non-standard port number. * <p/> * Alternately, hostnames may be stored in a hashed form which hides host * names and addresses should the file's contents be disclosed. Hashed * hostnames start with a `|' character. Only one hashed hostname may * appear on a single line and none of the above negation or wildcard * operators may be applied. * <p/> * Bits, exponent, and modulus are taken directly from the RSA host key; * they can be obtained, for example, from /etc/ssh/ssh_host_key.pub. The * optional comment field continues to the end of the line, and is not used. * <p/> * Lines starting with `#' and empty lines are ignored as comments. */ public class EntryFactory { EntryFactory() { } public HostEntry parseEntry(String line) throws IOException { if (isComment(line)) { return new CommentEntry(line); } final String[] split = line.split(" "); int i = 0; final Marker marker = Marker.fromString(split[i]); if (marker != null) { i++; } if(split.length < 3) { log.error("Error reading entry `{}`", line); return null; } final String hostnames = split[i++]; final String sType = split[i++]; KeyType type = KeyType.fromString(sType); PublicKey key; if (type != KeyType.UNKNOWN) { final String sKey = split[i++]; key = new Buffer.PlainBuffer(Base64.decode(sKey)).readPublicKey(); } else if (isBits(sType)) { type = KeyType.RSA; // int bits = Integer.valueOf(sType); final BigInteger e = new BigInteger(split[i++]); final BigInteger n = new BigInteger(split[i++]); try { final KeyFactory keyFactory = SecurityUtils.getKeyFactory("RSA"); key = keyFactory.generatePublic(new RSAPublicKeySpec(n, e)); } catch (Exception ex) { log.error("Error reading entry `{}`, could not create key", line, ex); return null; } } else { log.error("Error reading entry `{}`, could not determine type", line); return null; } if (isHashed(hostnames)) { return new HashedEntry(marker, hostnames, type, key); } else { return new SimpleEntry(marker, hostnames, type, key); } } private boolean isBits(String type) { try { Integer.parseInt(type); return true; } catch (NumberFormatException e) { return false; } } private boolean isComment(String line) { return line.isEmpty() || line.startsWith("#"); } public boolean isHashed(String line) { return line.startsWith("|1|"); } } public interface HostEntry { KeyType getType(); String getFingerprint(); boolean appliesTo(String host) throws IOException; boolean appliesTo(KeyType type, String host) throws IOException; boolean verify(PublicKey key) throws IOException; String getLine(); } public static class CommentEntry implements HostEntry { private final String comment; public CommentEntry(String comment) { this.comment = comment; } @Override public KeyType getType() { return KeyType.UNKNOWN; } @Override public String getFingerprint() { return null; } @Override public boolean appliesTo(String host) throws IOException { return false; } @Override public boolean appliesTo(KeyType type, String host) { return false; } @Override public boolean verify(PublicKey key) { return false; } @Override public String getLine() { return comment; } } public static abstract class AbstractEntry implements HostEntry { protected final OpenSSHKnownHosts.Marker marker; protected final KeyType type; protected final PublicKey key; public AbstractEntry(Marker marker, KeyType type, PublicKey key) { this.marker = marker; this.type = type; this.key = key; } @Override public KeyType getType() { return type; } @Override public String getFingerprint() { return SecurityUtils.getFingerprint(key); } @Override public boolean verify(PublicKey key) throws IOException { return key.equals(this.key) && marker != Marker.REVOKED; } public String getLine() { final StringBuilder line = new StringBuilder(); if (marker != null) line.append(marker.getMarkerString()).append(" "); line.append(getHostPart()); line.append(" ").append(type.toString()); line.append(" ").append(getKeyString()); return line.toString(); } private String getKeyString() { final Buffer.PlainBuffer buf = new Buffer.PlainBuffer().putPublicKey(key); return Base64.encodeBytes(buf.array(), buf.rpos(), buf.available()); } protected abstract String getHostPart(); } public static class SimpleEntry extends AbstractEntry { private final List<String> hosts; private final String hostnames; public SimpleEntry(Marker marker, String hostnames, KeyType type, PublicKey key) { super(marker, type, key); this.hostnames = hostnames; hosts = Arrays.asList(hostnames.split(",")); } @Override protected String getHostPart() { return hostnames; } @Override public boolean appliesTo(String host) throws IOException { return hosts.contains(host); } @Override public boolean appliesTo(KeyType type, String host) throws IOException { return type == this.type && hosts.contains(host); } } public static class HashedEntry extends AbstractEntry { private final MAC sha1 = new HMACSHA1(); private final String hashedHost; private final String salt; private byte[] saltyBytes; public HashedEntry(Marker marker, String hash, KeyType type, PublicKey key) throws SSHException { super(marker, type, key); this.hashedHost = hash; { final String[] hostParts = hashedHost.split("\\|"); if (hostParts.length != 4) throw new SSHException("Unrecognized format for hashed hostname"); salt = hostParts[2]; } } @Override public boolean appliesTo(String host) throws IOException { return hashedHost.equals(hashHost(host)); } @Override public boolean appliesTo(KeyType type, String host) throws IOException { return this.type == type && hashedHost.equals(hashHost(host)); } private String hashHost(String host) throws IOException { sha1.init(getSaltyBytes()); return "|1|" + salt + "|" + Base64.encodeBytes(sha1.doFinal(host.getBytes(IOUtils.UTF8))); } private byte[] getSaltyBytes() throws IOException { if (saltyBytes == null) { saltyBytes = Base64.decode(salt); } return saltyBytes; } @Override protected String getHostPart() { return hashedHost; } } public enum Marker { CA_CERT("@cert-authority"), REVOKED("@revoked"); private final String sMarker; Marker(String sMarker) { this.sMarker = sMarker; } public String getMarkerString() { return sMarker; } public static Marker fromString(String str) { for (Marker m: values()) if (m.sMarker.equals(str)) return m; return null; } } }