You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2016/02/14 15:16:54 UTC

[3/3] mina-sshd git commit: [SSHD-266] Support for known hosts

[SSHD-266] Support for known hosts


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/509c871e
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/509c871e
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/509c871e

Branch: refs/heads/master
Commit: 509c871eac5f9dff521891971fb24673135b52c3
Parents: 305850c
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Sun Feb 14 16:16:35 2016 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Sun Feb 14 16:16:35 2016 +0200

----------------------------------------------------------------------
 .../java/org/apache/sshd/client/SshClient.java  |   4 +-
 .../DefaultConfigFileHostEntryResolver.java     |  15 +-
 .../client/config/hosts/HostConfigEntry.java    | 448 +++---------
 .../client/config/hosts/HostPatternsHolder.java | 310 ++++++++
 .../client/config/hosts/KnownHostDigest.java    |  65 ++
 .../client/config/hosts/KnownHostEntry.java     | 271 +++++++
 .../client/config/hosts/KnownHostHashValue.java | 166 +++++
 .../DefaultKnownHostsServerKeyVerifier.java     |  89 +++
 .../KnownHostsServerKeyVerifier.java            | 720 +++++++++++++++++++
 .../client/keyverifier/ServerKeyVerifier.java   |  10 +-
 .../client/session/AbstractClientSession.java   |  11 +
 .../sshd/client/session/ClientSession.java      |  12 +
 .../common/config/keys/AuthorizedKeyEntry.java  | 361 ++++++++++
 .../org/apache/sshd/common/mac/BaseMac.java     |  14 +-
 .../java/org/apache/sshd/common/mac/Mac.java    |   2 +
 .../org/apache/sshd/common/util/Base64.java     |  98 ++-
 .../java/org/apache/sshd/common/util/Pair.java  |   2 +-
 .../sshd/common/util/buffer/BufferUtils.java    |  10 +-
 .../common/util/io/ModifiableFileWatcher.java   |   7 +-
 .../sshd/common/util/net/SshdSocketAddress.java |  41 ++
 .../server/config/keys/AuthorizedKeyEntry.java  | 399 ----------
 .../keys/AuthorizedKeysAuthenticator.java       |  41 ++
 .../DefaultAuthorizedKeysAuthenticator.java     |   3 +-
 .../java/org/apache/sshd/client/ClientTest.java |   7 +
 .../hosts/ConfigFileHostEntryResolverTest.java  |  10 +-
 .../hosts/HostConfigEntryResolverTest.java      |  12 +-
 .../config/hosts/HostConfigEntryTest.java       |  50 +-
 .../config/hosts/KnownHostHashValueTest.java    |  73 ++
 .../KnownHostsServerKeyVerifierTest.java        | 358 +++++++++
 .../org/apache/sshd/client/scp/ScpTest.java     |   4 +-
 .../config/keys/AuthorizedKeyEntryTest.java     | 137 ++++
 .../config/keys/AuthorizedKeysTestSupport.java  | 121 ++++
 .../org/apache/sshd/common/util/Base64Test.java |  73 ++
 .../config/keys/AuthorizedKeyEntryTest.java     | 135 ----
 .../keys/AuthorizedKeysAuthenticatorTest.java   |   2 +
 .../config/keys/AuthorizedKeysTestSupport.java  | 120 ----
 .../DefaultAuthorizedKeysAuthenticatorTest.java |   2 +
 .../apache/sshd/client/keyverifier/known_hosts  |  12 +
 .../sshd/common/config/keys/authorized_keys     |  16 +
 .../auth/pubkey/LdapPublickeyAuthenticator.java |   2 +-
 .../pubkey/LdapPublickeyAuthenticatorTest.java  |   2 +-
 41 files changed, 3123 insertions(+), 1112 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 5d567b1..d159fe1 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -73,6 +73,7 @@ import org.apache.sshd.client.config.keys.DefaultClientIdentitiesWatcher;
 import org.apache.sshd.client.future.ConnectFuture;
 import org.apache.sshd.client.future.DefaultConnectFuture;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
+import org.apache.sshd.client.session.AbstractClientSession;
 import org.apache.sshd.client.session.ClientConnectionServiceFactory;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.session.ClientSessionCreator;
@@ -590,8 +591,9 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
 
     protected void onConnectOperationComplete(IoSession ioSession, ConnectFuture connectFuture,
             String username, SocketAddress address, Collection<? extends KeyPair> identities, boolean useDefaultIdentities) {
-        ClientSession session = (ClientSession) AbstractSession.getSession(ioSession);
+        AbstractClientSession session = (AbstractClientSession) AbstractSession.getSession(ioSession);
         session.setUsername(username);
+        session.setConnectAddress(address);
 
         if (useDefaultIdentities) {
             setupDefaultSessionIdentities(session);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
index 6b1e86b..3d39b6d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
@@ -32,9 +32,9 @@ import org.apache.sshd.common.util.io.IoUtils;
 
 /**
  * Monitors the {@code ~/.ssh/config} file of the user currently running
- * the server, re-loading it if necessary. It also (optionally) enforces the same
- * permissions regime as {@code OpenSSH} does for the file permissions.
-
+ * the client, re-loading it if necessary. It also (optionally) enforces
+ * the same permissions regime as {@code OpenSSH}
+ *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class DefaultConfigFileHostEntryResolver extends ConfigFileHostEntryResolver {
@@ -48,8 +48,9 @@ public class DefaultConfigFileHostEntryResolver extends ConfigFileHostEntryResol
 
     /**
      * @param strict If {@code true} then makes sure that the containing folder
-     *               has 0700 access and the file 0600. <B>Note:</B> for <I>Windows</I> it
+     *               has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
      *               does not check these permissions
+     * @see #validateStrictConfigFilePermissions(Path, LinkOption...)
      */
     public DefaultConfigFileHostEntryResolver(boolean strict) {
         this(HostConfigEntry.getDefaultHostConfigFile(), strict);
@@ -64,6 +65,12 @@ public class DefaultConfigFileHostEntryResolver extends ConfigFileHostEntryResol
         this.strict = strict;
     }
 
+    /**
+     * @return If {@code true} then makes sure that the containing folder
+     * has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
+     * does not check these permissions
+     * @see #validateStrictConfigFilePermissions(Path, LinkOption...)
+     */
     public final boolean isStrict() {
         return strict;
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
index 52a8a83..1d3fc09 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
@@ -46,16 +46,12 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
 import org.apache.sshd.common.auth.MutableUserHolder;
 import org.apache.sshd.common.config.SshConfigFileReader;
 import org.apache.sshd.common.config.keys.IdentityUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.OsUtils;
-import org.apache.sshd.common.util.Pair;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.io.NoCloseInputStream;
@@ -68,34 +64,12 @@ import org.apache.sshd.common.util.io.NoCloseReader;
  * file format</A>
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class HostConfigEntry implements MutableUserHolder {
-
-    /**
-     * Used in a host pattern to denote zero or more consecutive characters
-     */
-    public static final char WILDCARD_PATTERN = '*';
-    public static final String ALL_HOSTS_PATTERN = String.valueOf(HostConfigEntry.WILDCARD_PATTERN);
-
+public class HostConfigEntry extends HostPatternsHolder implements MutableUserHolder {
     /**
      * Standard OpenSSH config file name
      */
     public static final String STD_CONFIG_FILENAME = "config";
 
-    /**
-     * Used in a host pattern to denote any <U>one</U> character
-     */
-    public static final char SINGLE_CHAR_PATTERN = '?';
-
-    /**
-     * Used to negate a host pattern
-     */
-    public static final char NEGATION_CHAR_PATTERN = '!';
-
-    /**
-     * The available pattern characters
-     */
-    public static final String PATTERN_CHARS = new String(new char[]{WILDCARD_PATTERN, SINGLE_CHAR_PATTERN, NEGATION_CHAR_PATTERN});
-
     public static final String HOST_CONFIG_PROP = "Host";
     public static final String HOST_NAME_CONFIG_PROP = "HostName";
     public static final String PORT_CONFIG_PROP = SshConfigFileReader.PORT_CONFIG_PROP;
@@ -130,7 +104,7 @@ public class HostConfigEntry implements MutableUserHolder {
     public static final char REMOTE_PORT_MACRO = 'p';
 
     private static final class LazyDefaultConfigFileHolder {
-        private static final Path KEYS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_CONFIG_FILENAME);
+        private static final Path CONFIG_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_CONFIG_FILENAME);
     }
 
     private String host;
@@ -138,7 +112,6 @@ public class HostConfigEntry implements MutableUserHolder {
     private int port;
     private String username;
     private Boolean exclusiveIdentites;
-    private Collection<Pair<Pattern, Boolean>> patterns = new LinkedList<>();
     private Collection<String> identities = Collections.emptyList();
     private Map<String, String> properties = Collections.emptyMap();
 
@@ -162,27 +135,12 @@ public class HostConfigEntry implements MutableUserHolder {
 
     public void setHost(String host) {
         this.host = host;
-        this.patterns = parsePatterns(parseConfigValue(host));
+        setPatterns(parsePatterns(parseConfigValue(host)));
     }
 
     public void setHost(Collection<String> patterns) {
         this.host = GenericUtils.join(ValidateUtils.checkNotNullAndNotEmpty(patterns, "No patterns"), ',');
-        this.patterns = parsePatterns(patterns);
-    }
-
-    public Collection<Pair<Pattern, Boolean>> getPatterns() {
-        return patterns;
-    }
-
-    /**
-     * Checks if a given host name / address matches the entry's host pattern(s)
-     *
-     * @param host The host name / address - ignored if {@code null}/empty
-     * @return {@code true} if the name / address matches the pattern(s)
-     * @see #isHostMatch(String, Pattern)
-     */
-    public boolean isHostMatch(String host) {
-        return isHostMatch(host, getPatterns());
+        setPatterns(parsePatterns(patterns));
     }
 
     /**
@@ -777,165 +735,6 @@ public class HostConfigEntry implements MutableUserHolder {
     }
 
     /**
-     * Finds the best match out of the given ones.
-     *
-     * @param matches The available matches - ignored if {@code null}/empty
-     * @return The best match or {@code null} if no matches or no best match found
-     * @see #findBestMatch(Iterator)
-     */
-    public static HostConfigEntry findBestMatch(Collection<? extends HostConfigEntry> matches) {
-        if (GenericUtils.isEmpty(matches)) {
-            return null;
-        } else {
-            return findBestMatch(matches.iterator());
-        }
-    }
-
-    /**
-     * Finds the best match out of the given ones.
-     *
-     * @param matches The available matches - ignored if {@code null}/empty
-     * @return The best match or {@code null} if no matches or no best match found
-     * @see #findBestMatch(Iterator)
-     */
-    public static HostConfigEntry findBestMatch(Iterable<? extends HostConfigEntry> matches) {
-        if (matches == null) {
-            return null;
-        } else {
-            return findBestMatch(matches.iterator());
-        }
-    }
-
-    /**
-     * Finds the best match out of the given ones. The best match is defined as one whose
-     * pattern is as <U>specific</U> as possible (if more than one match is available).
-     * I.e., a non-global match is preferred over global one, and a match with no wildcards
-     * is preferred over one with such a pattern.
-     *
-     * @param matches The available matches - ignored if {@code null}/empty
-     * @return The best match or {@code null} if no matches or no best match found
-     * @see #isSpecificHostPattern(String)
-     */
-    public static HostConfigEntry findBestMatch(Iterator<? extends HostConfigEntry> matches) {
-        if ((matches == null) || (!matches.hasNext())) {
-            return null;
-        }
-
-        HostConfigEntry candidate = matches.next();
-        int wildcardMatches = 0;
-        while (matches.hasNext()) {
-            HostConfigEntry entry = matches.next();
-            String entryPattern = entry.getHost();
-            String candidatePattern = candidate.getHost();
-            // prefer non-global entry over global entry
-            if (ALL_HOSTS_PATTERN.equalsIgnoreCase(candidatePattern)) {
-                // unlikely, but handle it
-                if (ALL_HOSTS_PATTERN.equalsIgnoreCase(entryPattern)) {
-                    wildcardMatches++;
-                } else {
-                    candidate = entry;
-                    wildcardMatches = 0;
-                }
-                continue;
-            }
-
-            if (isSpecificHostPattern(entryPattern)) {
-                // if both are specific then no best match
-                if (isSpecificHostPattern(candidatePattern)) {
-                    return null;
-                }
-
-                candidate = entry;
-                wildcardMatches = 0;
-                continue;
-            }
-
-            wildcardMatches++;
-        }
-
-        String candidatePattern = candidate.getHost();
-        // best match either has specific host or no wildcard matches
-        if ((wildcardMatches <= 0) || (isSpecificHostPattern(candidatePattern))) {
-            return candidate;
-        }
-
-        return null;
-    }
-
-    /**
-     * @param pattern The pattern to check - ignored if {@code null}/empty
-     * @return {@code true} if the pattern is not empty and contains no wildcard characters
-     * @see #WILDCARD_PATTERN
-     * @see #SINGLE_CHAR_PATTERN
-     * @see #SINGLE_CHAR_PATTERN
-     */
-    public static boolean isSpecificHostPattern(String pattern) {
-        if (GenericUtils.isEmpty(pattern)) {
-            return false;
-        }
-
-        for (int index = 0; index < PATTERN_CHARS.length(); index++) {
-            char ch = PATTERN_CHARS.charAt(index);
-            if (pattern.indexOf(ch) >= 0) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Locates all the matching entries for a give host name / address
-     *
-     * @param host The host name / address - ignored if {@code null}/empty
-     * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
-     * @return A {@link List} of all the matching entries
-     * @see #isHostMatch(String)
-     */
-    public static List<HostConfigEntry> findMatchingEntries(String host, HostConfigEntry ... entries) {
-        // TODO in Java-8 use Stream(s) + predicate
-        if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
-            return Collections.emptyList();
-        } else {
-            return findMatchingEntries(host, Arrays.asList(entries));
-        }
-    }
-
-    /**
-     * Locates all the matching entries for a give host name / address
-     *
-     * @param host The host name / address - ignored if {@code null}/empty
-     * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
-     * @return A {@link List} of all the matching entries
-     * @see #isHostMatch(String)
-     */
-    public static List<HostConfigEntry> findMatchingEntries(String host, Collection<? extends HostConfigEntry> entries) {
-        // TODO in Java-8 use Stream(s) + predicate
-        if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
-            return Collections.emptyList();
-        }
-
-        List<HostConfigEntry> matches = null;
-        for (HostConfigEntry entry : entries) {
-            if (!entry.isHostMatch(host)) {
-                continue;   // debug breakpoint
-            }
-
-            if (matches == null) {
-                matches = new ArrayList<>(entries.size());  // in case ALL of them match
-            }
-
-            matches.add(entry);
-        }
-
-        if (matches == null) {
-            return Collections.emptyList();
-        } else {
-            return matches;
-        }
-    }
-
-    /**
      * Resolves the effective target host
      *
      * @param originalName The original requested host
@@ -983,154 +782,6 @@ public class HostConfigEntry implements MutableUserHolder {
         }
     }
 
-    public static boolean isHostMatch(String host, Collection<Pair<Pattern, Boolean>> patterns) {
-        if (GenericUtils.isEmpty(patterns)) {
-            return false;
-        }
-
-        boolean matchFound = false;
-        for (Pair<Pattern, Boolean> pp : patterns) {
-            Boolean negated = pp.getSecond();
-            /*
-             * If already found a match we are interested only in negations
-             */
-            if (matchFound && (!negated.booleanValue())) {
-                continue;
-            }
-
-            if (!isHostMatch(host, pp.getFirst())) {
-                continue;
-            }
-
-            /*
-             * According to https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5:
-             *
-             *      If a negated entry is matched, then the Host entry is ignored,
-             *      regardless of whether any other patterns on the line match.
-             */
-            if (negated.booleanValue()) {
-                return false;
-            }
-
-            matchFound = true;
-        }
-
-        return matchFound;
-    }
-
-    /**
-     * Checks if a given host name / address matches a host pattern
-     *
-     * @param host The host name / address - ignored if {@code null}/empty
-     * @param pattern The host {@link Pattern} - ignored if {@code null}
-     * @return {@code true} if the name / address matches the pattern
-     */
-    public static boolean isHostMatch(String host, Pattern pattern) {
-        if (GenericUtils.isEmpty(host) || (pattern == null)) {
-            return false;
-        }
-
-        Matcher m = pattern.matcher(host);
-        return m.matches();
-    }
-
-    public static List<Pair<Pattern, Boolean>> parsePatterns(Collection<? extends CharSequence> patterns) {
-        if (GenericUtils.isEmpty(patterns)) {
-            return Collections.emptyList();
-        }
-
-        List<Pair<Pattern, Boolean>> result = new ArrayList<>(patterns.size());
-        for (CharSequence p : patterns) {
-            result.add(ValidateUtils.checkNotNull(toPattern(p), "No pattern for %s", p));
-        }
-
-        return result;
-    }
-
-    /**
-     * Converts a host pattern string to a regular expression matcher.
-     * <B>Note:</B> pattern matching is <U>case insensitive</U>
-     *
-     * @param pattern The original pattern string - ignored if {@code null}/empty
-     * @return The regular expression matcher {@link Pattern} and the indication
-     * whether it is a negating pattern or not - {@code null} if no original string
-     * @see #WILDCARD_PATTERN
-     * @see #SINGLE_CHAR_PATTERN
-     * @see #NEGATION_CHAR_PATTERN
-     */
-    public static Pair<Pattern, Boolean> toPattern(CharSequence pattern) {
-        if (GenericUtils.isEmpty(pattern)) {
-            return null;
-        }
-
-        StringBuilder sb = new StringBuilder(pattern.length());
-        boolean negated = false;
-        for (int curPos = 0; curPos < pattern.length(); curPos++) {
-            char ch = pattern.charAt(curPos);
-            ValidateUtils.checkTrue(isValidPatternChar(ch), "Invalid host pattern char in %s", pattern);
-
-            switch(ch) {
-                case '.':   // need to escape it
-                    sb.append('\\').append(ch);
-                    break;
-                case SINGLE_CHAR_PATTERN:
-                    sb.append('.');
-                    break;
-                case WILDCARD_PATTERN:
-                    sb.append(".*");
-                    break;
-                case NEGATION_CHAR_PATTERN:
-                    ValidateUtils.checkTrue(!negated, "Double negation in %s", pattern);
-                    ValidateUtils.checkTrue(curPos == 0, "Negation must be 1st char: %s", pattern);
-                    negated = true;
-                    break;
-                default:
-                    sb.append(ch);
-            }
-        }
-
-        return new Pair<Pattern, Boolean>(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), Boolean.valueOf(negated));
-    }
-
-    /**
-     * Checks if the given character is valid for a host pattern. Valid
-     * characters are:
-     * <UL>
-     *      <LI>A-Z</LI>
-     *      <LI>a-z</LI>
-     *      <LI>0-9</LI>
-     *      <LI>Underscore (_)</LI>
-     *      <LI>Hyphen (-)</LI>
-     *      <LI>Dot (.)</LI>
-     *      <LI>The {@link #WILDCARD_PATTERN}</LI>
-     *      <LI>The {@link #SINGLE_CHAR_PATTERN}</LI>
-     * </UL>
-     *
-     * @param ch The character to validate
-     * @return {@code true} if valid pattern character
-     */
-    public static boolean isValidPatternChar(char ch) {
-        if ((ch <= ' ') || (ch >= 0x7E)) {
-            return false;
-        }
-        if ((ch >= 'a') && (ch <= 'z')) {
-            return true;
-        }
-        if ((ch >= 'A') && (ch <= 'Z')) {
-            return true;
-        }
-        if ((ch >= '0') && (ch <= '9')) {
-            return true;
-        }
-        if ("-_.".indexOf(ch) >= 0) {
-            return true;
-        }
-        if (PATTERN_CHARS.indexOf(ch) >= 0) {
-            return true;
-        }
-        return false;
-    }
-
     public static List<HostConfigEntry> readHostConfigEntries(File file) throws IOException {
         return readHostConfigEntries(file.toPath(), IoUtils.EMPTY_OPEN_OPTIONS);
     }
@@ -1259,6 +910,92 @@ public class HostConfigEntry implements MutableUserHolder {
         }
     }
 
+    /**
+     * Finds the best match out of the given ones.
+     *
+     * @param matches The available matches - ignored if {@code null}/empty
+     * @return The best match or {@code null} if no matches or no best match found
+     * @see #findBestMatch(Iterator)
+     */
+    public static HostConfigEntry findBestMatch(Collection<? extends HostConfigEntry> matches) {
+        if (GenericUtils.isEmpty(matches)) {
+            return null;
+        } else {
+            return findBestMatch(matches.iterator());
+        }
+    }
+
+    /**
+     * Finds the best match out of the given ones.
+     *
+     * @param matches The available matches - ignored if {@code null}/empty
+     * @return The best match or {@code null} if no matches or no best match found
+     * @see #findBestMatch(Iterator)
+     */
+    public static HostConfigEntry findBestMatch(Iterable<? extends HostConfigEntry> matches) {
+        if (matches == null) {
+            return null;
+        } else {
+            return findBestMatch(matches.iterator());
+        }
+    }
+
+    /**
+     * Finds the best match out of the given ones. The best match is defined as one whose
+     * pattern is as <U>specific</U> as possible (if more than one match is available).
+     * I.e., a non-global match is preferred over global one, and a match with no wildcards
+     * is preferred over one with such a pattern.
+     *
+     * @param matches The available matches - ignored if {@code null}/empty
+     * @return The best match or {@code null} if no matches or no best match found
+     * @see #isSpecificHostPattern(String)
+     */
+    public static HostConfigEntry findBestMatch(Iterator<? extends HostConfigEntry> matches) {
+        if ((matches == null) || (!matches.hasNext())) {
+            return null;
+        }
+
+        HostConfigEntry candidate = matches.next();
+        int wildcardMatches = 0;
+        while (matches.hasNext()) {
+            HostConfigEntry entry = matches.next();
+            String entryPattern = entry.getHost();
+            String candidatePattern = candidate.getHost();
+            // prefer non-global entry over global entry
+            if (ALL_HOSTS_PATTERN.equalsIgnoreCase(candidatePattern)) {
+                // unlikely, but handle it
+                if (ALL_HOSTS_PATTERN.equalsIgnoreCase(entryPattern)) {
+                    wildcardMatches++;
+                } else {
+                    candidate = entry;
+                    wildcardMatches = 0;
+                }
+                continue;
+            }
+
+            if (isSpecificHostPattern(entryPattern)) {
+                // if both are specific then no best match
+                if (isSpecificHostPattern(candidatePattern)) {
+                    return null;
+                }
+
+                candidate = entry;
+                wildcardMatches = 0;
+                continue;
+            }
+
+            wildcardMatches++;
+        }
+
+        String candidatePattern = candidate.getHost();
+        // best match either has specific host or no wildcard matches
+        if ((wildcardMatches <= 0) || (isSpecificHostPattern(candidatePattern))) {
+            return candidate;
+        }
+
+        return null;
+    }
+
     public static List<HostConfigEntry> updateEntriesList(List<HostConfigEntry> entries, HostConfigEntry curEntry) {
         if (curEntry == null) {
             return entries;
@@ -1271,6 +1008,7 @@ public class HostConfigEntry implements MutableUserHolder {
         entries.add(curEntry);
         return entries;
     }
+
     public static void writeHostConfigEntries(File file, Collection<? extends HostConfigEntry> entries) throws IOException {
         writeHostConfigEntries(ValidateUtils.checkNotNull(file, "No file").toPath(), entries, IoUtils.EMPTY_OPEN_OPTIONS);
     }
@@ -1420,10 +1158,10 @@ public class HostConfigEntry implements MutableUserHolder {
     }
 
     /**
-     * @return The default {@link Path} location of the OpenSSH authorized keys file
+     * @return The default {@link Path} location of the OpenSSH hosts entries configuration file
      */
     @SuppressWarnings("synthetic-access")
     public static Path getDefaultHostConfigFile() {
-        return LazyDefaultConfigFileHolder.KEYS_FILE;
+        return LazyDefaultConfigFileHolder.CONFIG_FILE;
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
new file mode 100644
index 0000000..4878458
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.client.config.hosts;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class HostPatternsHolder {
+
+    /**
+     * Used in a host pattern to denote zero or more consecutive characters
+     */
+    public static final char WILDCARD_PATTERN = '*';
+    public static final String ALL_HOSTS_PATTERN = String.valueOf(WILDCARD_PATTERN);
+
+    /**
+     * Used in a host pattern to denote any <U>one</U> character
+     */
+    public static final char SINGLE_CHAR_PATTERN = '?';
+
+    /**
+     * Used to negate a host pattern
+     */
+    public static final char NEGATION_CHAR_PATTERN = '!';
+
+    /**
+     * The available pattern characters
+     */
+    public static final String PATTERN_CHARS = new String(new char[]{WILDCARD_PATTERN, SINGLE_CHAR_PATTERN, NEGATION_CHAR_PATTERN});
+
+    private Collection<Pair<Pattern, Boolean>> patterns = new LinkedList<>();
+
+    protected HostPatternsHolder() {
+        super();
+    }
+
+    public Collection<Pair<Pattern, Boolean>> getPatterns() {
+        return patterns;
+    }
+
+    public void setPatterns(Collection<Pair<Pattern, Boolean>> patterns) {
+        this.patterns = patterns;
+    }
+
+    /**
+     * Checks if a given host name / address matches the entry's host pattern(s)
+     *
+     * @param host The host name / address - ignored if {@code null}/empty
+     * @return {@code true} if the name / address matches the pattern(s)
+     * @see #isHostMatch(String, Pattern)
+     */
+    public boolean isHostMatch(String host) {
+        return isHostMatch(host, getPatterns());
+    }
+
+    /**
+     * @param pattern The pattern to check - ignored if {@code null}/empty
+     * @return {@code true} if the pattern is not empty and contains no wildcard characters
+     * @see #WILDCARD_PATTERN
+     * @see #SINGLE_CHAR_PATTERN
+     * @see #SINGLE_CHAR_PATTERN
+     */
+    public static boolean isSpecificHostPattern(String pattern) {
+        if (GenericUtils.isEmpty(pattern)) {
+            return false;
+        }
+
+        for (int index = 0; index < PATTERN_CHARS.length(); index++) {
+            char ch = PATTERN_CHARS.charAt(index);
+            if (pattern.indexOf(ch) >= 0) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Locates all the matching entries for a give host name / address
+     *
+     * @param host The host name / address - ignored if {@code null}/empty
+     * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
+     * @return A {@link List} of all the matching entries
+     * @see #isHostMatch(String)
+     */
+    public static List<HostConfigEntry> findMatchingEntries(String host, HostConfigEntry ... entries) {
+        // TODO in Java-8 use Stream(s) + predicate
+        if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
+            return Collections.emptyList();
+        } else {
+            return findMatchingEntries(host, Arrays.asList(entries));
+        }
+    }
+
+    /**
+     * Locates all the matching entries for a give host name / address
+     *
+     * @param host The host name / address - ignored if {@code null}/empty
+     * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
+     * @return A {@link List} of all the matching entries
+     * @see #isHostMatch(String)
+     */
+    public static List<HostConfigEntry> findMatchingEntries(String host, Collection<? extends HostConfigEntry> entries) {
+        // TODO in Java-8 use Stream(s) + predicate
+        if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
+            return Collections.emptyList();
+        }
+
+        List<HostConfigEntry> matches = null;
+        for (HostConfigEntry entry : entries) {
+            if (!entry.isHostMatch(host)) {
+                continue;   // debug breakpoint
+            }
+
+            if (matches == null) {
+                matches = new ArrayList<>(entries.size());  // in case ALL of them match
+            }
+
+            matches.add(entry);
+        }
+
+        if (matches == null) {
+            return Collections.emptyList();
+        } else {
+            return matches;
+        }
+    }
+
+    public static boolean isHostMatch(String host, Collection<Pair<Pattern, Boolean>> patterns) {
+        if (GenericUtils.isEmpty(patterns)) {
+            return false;
+        }
+
+        boolean matchFound = false;
+        for (Pair<Pattern, Boolean> pp : patterns) {
+            Boolean negated = pp.getSecond();
+            /*
+             * If already found a match we are interested only in negations
+             */
+            if (matchFound && (!negated.booleanValue())) {
+                continue;
+            }
+
+            if (!isHostMatch(host, pp.getFirst())) {
+                continue;
+            }
+
+            /*
+             * According to https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5:
+             *
+             *      If a negated entry is matched, then the Host entry is ignored,
+             *      regardless of whether any other patterns on the line match.
+             */
+            if (negated.booleanValue()) {
+                return false;
+            }
+
+            matchFound = true;
+        }
+
+        return matchFound;
+    }
+
+    /**
+     * Checks if a given host name / address matches a host pattern
+     *
+     * @param host The host name / address - ignored if {@code null}/empty
+     * @param pattern The host {@link Pattern} - ignored if {@code null}
+     * @return {@code true} if the name / address matches the pattern
+     */
+    public static boolean isHostMatch(String host, Pattern pattern) {
+        if (GenericUtils.isEmpty(host) || (pattern == null)) {
+            return false;
+        }
+
+        Matcher m = pattern.matcher(host);
+        return m.matches();
+    }
+
+    public static List<Pair<Pattern, Boolean>> parsePatterns(CharSequence ... patterns) {
+        return parsePatterns(GenericUtils.isEmpty(patterns) ? Collections.<CharSequence>emptyList() : Arrays.asList(patterns));
+    }
+
+    public static List<Pair<Pattern, Boolean>> parsePatterns(Collection<? extends CharSequence> patterns) {
+        if (GenericUtils.isEmpty(patterns)) {
+            return Collections.emptyList();
+        }
+
+        List<Pair<Pattern, Boolean>> result = new ArrayList<>(patterns.size());
+        for (CharSequence p : patterns) {
+            result.add(ValidateUtils.checkNotNull(toPattern(p), "No pattern for %s", p));
+        }
+
+        return result;
+    }
+
+    /**
+     * Converts a host pattern string to a regular expression matcher.
+     * <B>Note:</B> pattern matching is <U>case insensitive</U>
+     *
+     * @param pattern The original pattern string - ignored if {@code null}/empty
+     * @return The regular expression matcher {@link Pattern} and the indication
+     * whether it is a negating pattern or not - {@code null} if no original string
+     * @see #WILDCARD_PATTERN
+     * @see #SINGLE_CHAR_PATTERN
+     * @see #NEGATION_CHAR_PATTERN
+     */
+    public static Pair<Pattern, Boolean> toPattern(CharSequence pattern) {
+        if (GenericUtils.isEmpty(pattern)) {
+            return null;
+        }
+
+        StringBuilder sb = new StringBuilder(pattern.length());
+        boolean negated = false;
+        for (int curPos = 0; curPos < pattern.length(); curPos++) {
+            char ch = pattern.charAt(curPos);
+            ValidateUtils.checkTrue(isValidPatternChar(ch), "Invalid host pattern char in %s", pattern);
+
+            switch(ch) {
+                case '.':   // need to escape it
+                    sb.append('\\').append(ch);
+                    break;
+                case SINGLE_CHAR_PATTERN:
+                    sb.append('.');
+                    break;
+                case WILDCARD_PATTERN:
+                    sb.append(".*");
+                    break;
+                case NEGATION_CHAR_PATTERN:
+                    ValidateUtils.checkTrue(!negated, "Double negation in %s", pattern);
+                    ValidateUtils.checkTrue(curPos == 0, "Negation must be 1st char: %s", pattern);
+                    negated = true;
+                    break;
+                default:
+                    sb.append(ch);
+            }
+        }
+
+        return new Pair<Pattern, Boolean>(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), Boolean.valueOf(negated));
+    }
+
+    /**
+     * Checks if the given character is valid for a host pattern. Valid
+     * characters are:
+     * <UL>
+     *      <LI>A-Z</LI>
+     *      <LI>a-z</LI>
+     *      <LI>0-9</LI>
+     *      <LI>Underscore (_)</LI>
+     *      <LI>Hyphen (-)</LI>
+     *      <LI>Dot (.)</LI>
+     *      <LI>The {@link #WILDCARD_PATTERN}</LI>
+     *      <LI>The {@link #SINGLE_CHAR_PATTERN}</LI>
+     * </UL>
+     *
+     * @param ch The character to validate
+     * @return {@code true} if valid pattern character
+     */
+    public static boolean isValidPatternChar(char ch) {
+        if ((ch <= ' ') || (ch >= 0x7E)) {
+            return false;
+        }
+        if ((ch >= 'a') && (ch <= 'z')) {
+            return true;
+        }
+        if ((ch >= 'A') && (ch <= 'Z')) {
+            return true;
+        }
+        if ((ch >= '0') && (ch <= '9')) {
+            return true;
+        }
+        if ("-_.".indexOf(ch) >= 0) {
+            return true;
+        }
+        if (PATTERN_CHARS.indexOf(ch) >= 0) {
+            return true;
+        }
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
new file mode 100644
index 0000000..3005f47
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.client.config.hosts;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.mac.BuiltinMacs;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * Available digesters for known hosts entries
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public enum KnownHostDigest implements NamedFactory<Mac> {
+    SHA1("1", BuiltinMacs.hmacsha1);
+
+    public static final Set<KnownHostDigest> VALUES =
+            Collections.unmodifiableSet(EnumSet.allOf(KnownHostDigest.class));
+
+    private final String name;
+    private final Factory<Mac> factory;
+
+    KnownHostDigest(String name, Factory<Mac> factory) {
+        this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No name");
+        this.factory = ValidateUtils.checkNotNull(factory, "No factory");
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public Mac create() {
+        return factory.create();
+    }
+
+    public static KnownHostDigest fromName(String name) {
+        return NamedResource.Utils.findByName(name, String.CASE_INSENSITIVE_ORDER, VALUES);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
new file mode 100644
index 0000000..da77bac
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.client.config.hosts;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StreamCorruptedException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseReader;
+
+/**
+ * Contains a representation of an entry in the <code>known_hosts</code> file
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="http://www.manpagez.com/man/8/sshd/">sshd(8) man page</A>
+ */
+public class KnownHostEntry extends HostPatternsHolder {
+    /**
+     * Character that denotes that start of a marker
+     */
+    public static final char MARKER_INDICATOR = '@';
+
+    /**
+     * Standard OpenSSH config file name
+     */
+    public static final String STD_HOSTS_FILENAME = "known_hosts";
+
+    private static final class LazyDefaultConfigFileHolder {
+        private static final Path HOSTS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_HOSTS_FILENAME);
+    }
+
+    private String line;
+    private String marker;
+    private AuthorizedKeyEntry keyEntry;
+    private KnownHostHashValue hashedEntry;
+
+    public KnownHostEntry() {
+        super();
+    }
+
+    /**
+     * @param line The original line from which this entry was created
+     */
+    public KnownHostEntry(String line) {
+        this.line = line;
+    }
+
+    /**
+     * @return The original line from which this entry was created
+     */
+    public String getConfigLine() {
+        return line;
+    }
+
+    public void setConfigLine(String line) {
+        this.line = line;
+    }
+
+    public String getMarker() {
+        return marker;
+    }
+
+    public void setMarker(String marker) {
+        this.marker = marker;
+    }
+
+    public AuthorizedKeyEntry getKeyEntry() {
+        return keyEntry;
+    }
+
+    public void setKeyEntry(AuthorizedKeyEntry keyEntry) {
+        this.keyEntry = keyEntry;
+    }
+
+    public KnownHostHashValue getHashedEntry() {
+        return hashedEntry;
+    }
+
+    public void setHashedEntry(KnownHostHashValue hashedEntry) {
+        this.hashedEntry = hashedEntry;
+    }
+
+    @Override
+    public boolean isHostMatch(String host) {
+        if (super.isHostMatch(host)) {
+            return true;
+        }
+
+        KnownHostHashValue hash = getHashedEntry();
+        return (hash != null) && hash.isHostMatch(host);
+    }
+
+    @Override
+    public String toString() {
+        return getConfigLine();
+    }
+
+    /**
+     * @return The default {@link Path} location of the OpenSSH known hosts file
+     */
+    @SuppressWarnings("synthetic-access")
+    public static Path getDefaultKnownHostsFile() {
+        return LazyDefaultConfigFileHolder.HOSTS_FILE;
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(File file) throws IOException {
+        return readKnownHostEntries(file.toPath(), IoUtils.EMPTY_OPEN_OPTIONS);
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(Path path, OpenOption... options) throws IOException {
+        try (InputStream input = Files.newInputStream(path, options)) {
+            return readKnownHostEntries(input, true);
+        }
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(URL url) throws IOException {
+        try (InputStream input = url.openStream()) {
+            return readKnownHostEntries(input, true);
+        }
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(String filePath) throws IOException {
+        try (InputStream inStream = new FileInputStream(filePath)) {
+            return readKnownHostEntries(inStream, true);
+        }
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(InputStream inStream, boolean okToClose) throws IOException {
+        try (Reader reader = new InputStreamReader(NoCloseInputStream.resolveInputStream(inStream, okToClose), StandardCharsets.UTF_8)) {
+            return readKnownHostEntries(reader, true);
+        }
+    }
+
+    public static List<KnownHostEntry> readKnownHostEntries(Reader rdr, boolean okToClose) throws IOException {
+        try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
+            return readKnownHostEntries(buf);
+        }
+    }
+
+    /**
+     * Reads configuration entries
+     *
+     * @param rdr The {@link BufferedReader} to use
+     * @return The {@link List} of read {@link KnownHostEntry}-ies
+     * @throws IOException If failed to parse the read configuration
+     */
+    public static List<KnownHostEntry> readKnownHostEntries(BufferedReader rdr) throws IOException {
+        List<KnownHostEntry> entries = null;
+
+        int lineNumber = 1;
+        for (String line = rdr.readLine(); line != null; line = rdr.readLine(), lineNumber++) {
+            line = GenericUtils.trimToEmpty(line);
+            if (GenericUtils.isEmpty(line)) {
+                continue;
+            }
+
+            int pos = line.indexOf(SshConfigFileReader.COMMENT_CHAR);
+            if (pos == 0) {
+                continue;
+            }
+
+            if (pos > 0) {
+                line = line.substring(0, pos);
+                line = line.trim();
+            }
+
+            try {
+                KnownHostEntry entry = parseKnownHostEntry(line);
+                if (entry == null) {
+                    continue;
+                }
+
+                if (entries == null) {
+                    entries = new ArrayList<>();
+                }
+                entries.add(entry);
+            } catch (RuntimeException | Error e) {   // TODO consider consulting a user callback
+                throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
+                        + " to parse line #" + lineNumber + " '" + line + "': " + e.getMessage());
+            }
+        }
+
+        if (entries == null) {
+            return Collections.emptyList();
+        } else {
+            return entries;
+        }
+    }
+
+    public static KnownHostEntry parseKnownHostEntry(String line) {
+        return parseKnownHostEntry(GenericUtils.isEmpty(line) ? null : new KnownHostEntry(), line);
+    }
+
+    public static <E extends KnownHostEntry> E parseKnownHostEntry(E entry, String data) {
+        String line = data;
+        if (GenericUtils.isEmpty(line) || (line.charAt(0) == PublicKeyEntry.COMMENT_CHAR)) {
+            return entry;
+        }
+
+        entry.setConfigLine(line);
+
+        if (line.charAt(0) == MARKER_INDICATOR) {
+            int pos = line.indexOf(' ');
+            ValidateUtils.checkTrue(pos > 0, "Missing marker name end delimiter in line=%s", data);
+            ValidateUtils.checkTrue(pos > 1, "No marker name after indicator in line=%s", data);
+            entry.setMarker(line.substring(1, pos));
+            line = line.substring(pos + 1).trim();
+        } else {
+            entry.setMarker(null);
+        }
+
+        int pos = line.indexOf(' ');
+        ValidateUtils.checkTrue(pos > 0, "Missing host patterns end delimiter in line=%s", data);
+        String hostPattern = line.substring(0, pos);
+        line = line.substring(pos + 1).trim();
+
+        if (hostPattern.charAt(0) == KnownHostHashValue.HASHED_HOST_DELIMITER) {
+            KnownHostHashValue hash =
+                    ValidateUtils.checkNotNull(KnownHostHashValue.parse(hostPattern),
+                            "Failed to extract host hash value from line=%s", data);
+            entry.setHashedEntry(hash);
+            entry.setPatterns(null);
+        } else {
+            entry.setHashedEntry(null);
+            entry.setPatterns(parsePatterns(GenericUtils.split(hostPattern, ',')));
+        }
+
+        AuthorizedKeyEntry key =
+                ValidateUtils.checkNotNull(AuthorizedKeyEntry.parseAuthorizedKeyEntry(line),
+                        "No valid key entry recovered from line=%s", data);
+        entry.setKeyEntry(key);
+        return entry;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
new file mode 100644
index 0000000..f5fa825
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.client.config.hosts;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.RuntimeSshException;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.util.Base64;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.NumberUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class KnownHostHashValue {
+    /**
+     * Character used to indicate a hashed host pattern
+     */
+    public static final char HASHED_HOST_DELIMITER = '|';
+
+    public static final NamedFactory<Mac> DEFAULT_DIGEST = KnownHostDigest.SHA1;
+
+    private NamedFactory<Mac> digester = DEFAULT_DIGEST;
+    private byte[] saltValue;
+    private byte[] digestValue;
+
+    public KnownHostHashValue() {
+        super();
+    }
+
+    public NamedFactory<Mac> getDigester() {
+        return digester;
+    }
+
+    public void setDigester(NamedFactory<Mac> digester) {
+        this.digester = digester;
+    }
+
+    public byte[] getSaltValue() {
+        return saltValue;
+    }
+
+    public void setSaltValue(byte[] saltValue) {
+        this.saltValue = saltValue;
+    }
+
+    public byte[] getDigestValue() {
+        return digestValue;
+    }
+
+    public void setDigestValue(byte[] digestValue) {
+        this.digestValue = digestValue;
+    }
+
+    /**
+     * Checks if the host matches the hash
+     *
+     * @param host The host name/address - ignored if {@code null}/empty
+     * @return {@code true} if host matches the hash
+     * @throws RuntimeException If entry not properly initialized
+     */
+    public boolean isHostMatch(String host) {
+        if (GenericUtils.isEmpty(host)) {
+            return false;
+        }
+
+        try {
+            byte[] expected = getDigestValue();
+            byte[] actual = calculateHashValue(host, getDigester(), getSaltValue());
+            return Arrays.equals(expected, actual);
+        } catch (Throwable t) {
+            if (t instanceof RuntimeException) {
+                throw (RuntimeException) t;
+            }
+            throw new RuntimeSshException("Failed (" + t.getClass().getSimpleName() + ")"
+                    + " to calculate hash value: " + t.getMessage(), t);
+        }
+    }
+
+    @Override
+    public String toString() {
+        if ((getDigester() == null) || NumberUtils.isEmpty(getSaltValue()) || NumberUtils.isEmpty(getDigestValue())) {
+            return Objects.toString(getDigester(), null)
+                 + "-" + BufferUtils.toHex(':', getSaltValue())
+                 + "-" + BufferUtils.toHex(':', getDigestValue());
+        }
+
+        try {
+            return append(new StringBuilder(Byte.MAX_VALUE), this).toString();
+        } catch (IOException | RuntimeException e) {    // unexpected
+            return e.getClass().getSimpleName() + ": " + e.getMessage();
+        }
+    }
+
+    // see http://nms.lcs.mit.edu/projects/ssh/README.hashed-hosts
+    public static byte[] calculateHashValue(String host, Factory<? extends Mac> factory, byte[] salt) throws Exception {
+        return calculateHashValue(host, factory.create(), salt);
+    }
+
+    public static byte[] calculateHashValue(String host, Mac mac, byte[] salt) throws Exception {
+        mac.init(salt);
+
+        byte[] hostBytes = host.getBytes(StandardCharsets.UTF_8);
+        mac.update(hostBytes);
+        return mac.doFinal();
+    }
+
+    public static <A extends Appendable> A append(A sb, KnownHostHashValue hashValue) throws IOException {
+        return (hashValue == null) ? sb : append(sb, hashValue.getDigester(), hashValue.getSaltValue(), hashValue.getDigestValue());
+    }
+
+    public static <A extends Appendable> A append(A sb, NamedResource factory, byte[] salt, byte[] digest) throws IOException {
+        sb.append(HASHED_HOST_DELIMITER).append(factory.getName());
+        sb.append(HASHED_HOST_DELIMITER).append(Base64.encodeToString(salt));
+        sb.append(HASHED_HOST_DELIMITER).append(Base64.encodeToString(digest));
+        return sb;
+    }
+
+    public static KnownHostHashValue parse(String pattern) {
+        return parse(pattern, GenericUtils.isEmpty(pattern) ? null : new KnownHostHashValue());
+    }
+
+    public static <V extends KnownHostHashValue> V parse(String pattern, V value) {
+        if (GenericUtils.isEmpty(pattern)) {
+            return value;
+        }
+
+        String[] components = GenericUtils.split(pattern, HASHED_HOST_DELIMITER);
+        ValidateUtils.checkTrue(components.length == 4 /* 1st one is empty */, "Invalid hash pattern (insufficient data): %s", pattern);
+        ValidateUtils.checkTrue(GenericUtils.isEmpty(components[0]), "Invalid hash pattern (unexpected extra data): %s", pattern);
+
+        NamedFactory<Mac> factory =
+                ValidateUtils.checkNotNull(KnownHostDigest.fromName(components[1]),
+                        "Invalid hash pattern (unknwon digest): %s", pattern);
+        value.setDigester(factory);
+        value.setSaltValue(Base64.decodeString(components[2]));
+        value.setDigestValue(Base64.decodeString(components[3]));
+        return value;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java
new file mode 100644
index 0000000..f10645b
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.client.keyverifier;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sshd.client.config.hosts.KnownHostEntry;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+
+/**
+ * Monitors the {@code ~/.ssh/known_hosts} file of the user currently running
+ * the client, updating and re-loading it if necessary. It also (optionally)
+ * enforces the same permissions regime as {@code OpenSSH}.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultKnownHostsServerKeyVerifier extends KnownHostsServerKeyVerifier {
+    private final boolean strict;
+
+    public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate) {
+        this(delegate, true);
+    }
+
+    public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate, boolean strict) {
+        this(delegate, strict, KnownHostEntry.getDefaultKnownHostsFile(), IoUtils.getLinkOptions(false));
+    }
+
+    public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate, boolean strict, File file) {
+        this(delegate, strict, ValidateUtils.checkNotNull(file, "No file provided").toPath(), IoUtils.getLinkOptions(false));
+    }
+
+    public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate, boolean strict, Path file, LinkOption... options) {
+        super(delegate, file, options);
+        this.strict = strict;
+    }
+
+    /**
+     * @return If {@code true} then makes sure that the containing folder
+     * has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
+     * does not check these permissions
+     * @see #validateStrictConfigFilePermissions(Path, LinkOption...)
+     */
+    public final boolean isStrict() {
+        return strict;
+    }
+
+    @Override
+    protected List<HostEntryPair> reloadKnownHosts(Path file) throws IOException, GeneralSecurityException {
+        if (isStrict()) {
+            if (log.isDebugEnabled()) {
+                log.debug("reloadKnownHosts({}) check permissions", file);
+            }
+
+            Pair<String, Object> violation = validateStrictConfigFilePermissions(file);
+            if (violation != null) {
+                log.warn("reloadKnownHosts({}) invalid file permissions: {}", file, violation.getFirst());
+                updateReloadAttributes();
+                return Collections.emptyList();
+            }
+        }
+
+        return super.reloadKnownHosts(file);
+    }
+}