Added support for wildcard host entries in known_hosts (Fixes #331)

This commit is contained in:
Jeroen van Erp
2017-05-22 14:11:22 +02:00
parent 9d4f8fc46a
commit 7b535a8db3
4 changed files with 249 additions and 120 deletions

View File

@@ -0,0 +1,151 @@
/*
* 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 com.hierynomus.sshj.transport.verification;
import net.schmizz.sshj.common.Base64;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.common.SSHException;
import net.schmizz.sshj.transport.mac.HMACSHA1;
import net.schmizz.sshj.transport.mac.MAC;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class KnownHostMatchers {
public static HostMatcher createMatcher(String hostEntry) throws SSHException {
if (hostEntry.contains(",")) {
return new AnyHostMatcher(hostEntry);
}
if (hostEntry.startsWith("!")) {
return new NegateHostMatcher(hostEntry);
}
if (hostEntry.startsWith("|1|")) {
return new HashedHostMatcher(hostEntry);
}
if (hostEntry.contains("*") || hostEntry.contains("?")) {
return new WildcardHostMatcher(hostEntry);
}
return new EquiHostMatcher(hostEntry);
}
public interface HostMatcher {
boolean match(String hostname) throws IOException;
}
private static class EquiHostMatcher implements HostMatcher {
private String host;
public EquiHostMatcher(String host) {
this.host = host;
}
@Override
public boolean match(String hostname) {
return host.equals(hostname);
}
}
private static class HashedHostMatcher implements HostMatcher {
private final MAC sha1 = new HMACSHA1();
private final String hash;
private final String salt;
private byte[] saltyBytes;
HashedHostMatcher(String hash) throws SSHException {
this.hash = hash;
final String[] hostParts = hash.split("\\|");
if (hostParts.length != 4) {
throw new SSHException("Unrecognized format for hashed hostname");
}
salt = hostParts[2];
}
@Override
public boolean match(String hostname) throws IOException {
return hash.equals(hashHost(hostname));
}
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;
}
}
private static class AnyHostMatcher implements HostMatcher {
private final List<HostMatcher> matchers;
AnyHostMatcher(String hostEntry) throws SSHException {
matchers = new ArrayList<HostMatcher>();
for (String subEntry : hostEntry.split(",")) {
matchers.add(KnownHostMatchers.createMatcher(subEntry));
}
}
@Override
public boolean match(String hostname) throws IOException {
for (HostMatcher matcher : matchers) {
if (matcher.match(hostname)) {
return true;
}
}
return false;
}
}
private static class NegateHostMatcher implements HostMatcher {
private final HostMatcher matcher;
NegateHostMatcher(String hostEntry) throws SSHException {
this.matcher = createMatcher(hostEntry.substring(1));
}
@Override
public boolean match(String hostname) throws IOException {
return !matcher.match(hostname);
}
}
private static class WildcardHostMatcher implements HostMatcher {
private final Pattern pattern;
public WildcardHostMatcher(String hostEntry) {
this.pattern = Pattern.compile(hostEntry.replace(".", "\\.").replace("*", ".*").replace("?", "."));
}
@Override
public boolean match(String hostname) throws IOException {
return pattern.matcher(hostname).matches();
}
@Override
public String toString() {
return "WildcardHostMatcher[" + pattern + ']';
}
}
}

View File

@@ -48,7 +48,7 @@ public class ConsoleKnownHostsVerifier
}
if (response.equalsIgnoreCase(YES)) {
try {
entries().add(new SimpleEntry(null, hostname, KeyType.fromKey(key), key));
entries().add(new HostEntry(null, hostname, KeyType.fromKey(key), key));
write();
console.printf("Warning: Permanently added '%s' (%s) to the list of known hosts.\n", hostname, type);
} catch (IOException e) {
@@ -60,7 +60,7 @@ public class ConsoleKnownHostsVerifier
}
@Override
protected boolean hostKeyChangedAction(HostEntry entry, String hostname, PublicKey key) {
protected boolean hostKeyChangedAction(KnownHostEntry entry, String hostname, PublicKey key) {
final KeyType type = KeyType.fromKey(key);
final String fp = SecurityUtils.getFingerprint(key);
final String path = getFile().getAbsolutePath();

View File

@@ -15,9 +15,8 @@
*/
package net.schmizz.sshj.transport.verification;
import com.hierynomus.sshj.transport.verification.KnownHostMatchers;
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.*;
@@ -26,7 +25,6 @@ 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;
/**
@@ -40,7 +38,7 @@ public class OpenSSHKnownHosts
protected final Logger log;
protected final File khFile;
protected final List<HostEntry> entries = new ArrayList<HostEntry>();
protected final List<KnownHostEntry> entries = new ArrayList<KnownHostEntry>();
public OpenSSHKnownHosts(File khFile)
throws IOException {
@@ -59,7 +57,7 @@ public class OpenSSHKnownHosts
String line;
while ((line = br.readLine()) != null) {
try {
HostEntry entry = entryFactory.parseEntry(line);
KnownHostEntry entry = entryFactory.parseEntry(line);
if (entry != null) {
entries.add(entry);
}
@@ -83,12 +81,13 @@ public class OpenSSHKnownHosts
public boolean verify(final String hostname, final int port, final PublicKey key) {
final KeyType type = KeyType.fromKey(key);
if (type == KeyType.UNKNOWN)
if (type == KeyType.UNKNOWN) {
return false;
}
final String adjustedHostname = (port != 22) ? "[" + hostname + "]:" + port : hostname;
for (HostEntry e : entries) {
for (KnownHostEntry e : entries) {
try {
if (e.appliesTo(type, adjustedHostname))
return e.verify(key) || hostKeyChangedAction(e, adjustedHostname, key);
@@ -105,12 +104,12 @@ public class OpenSSHKnownHosts
return false;
}
protected boolean hostKeyChangedAction(HostEntry entry, String hostname, PublicKey key) {
protected boolean hostKeyChangedAction(KnownHostEntry entry, String hostname, PublicKey key) {
log.warn("Host key for `{}` has changed!", hostname);
return false;
}
public List<HostEntry> entries() {
public List<KnownHostEntry> entries() {
return entries;
}
@@ -120,7 +119,7 @@ public class OpenSSHKnownHosts
throws IOException {
final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(khFile));
try {
for (HostEntry entry : entries)
for (KnownHostEntry entry : entries)
bos.write((entry.getLine() + LS).getBytes(IOUtils.UTF8));
} finally {
bos.close();
@@ -130,7 +129,7 @@ public class OpenSSHKnownHosts
/**
* Append a single entry
*/
public void write(HostEntry entry)
public void write(KnownHostEntry entry)
throws IOException {
final BufferedWriter writer = new BufferedWriter(new FileWriter(khFile, true));
try {
@@ -185,7 +184,7 @@ public class OpenSSHKnownHosts
EntryFactory() {
}
public HostEntry parseEntry(String line)
public KnownHostEntry parseEntry(String line)
throws IOException {
if (isComment(line)) {
return new CommentEntry(line);
@@ -228,11 +227,7 @@ public class OpenSSHKnownHosts
return null;
}
if (isHashed(hostnames)) {
return new HashedEntry(marker, hostnames, type, key);
} else {
return new SimpleEntry(marker, hostnames, type, key);
}
return new HostEntry(marker, hostnames, type, key);
}
private boolean isBits(String type) {
@@ -254,25 +249,22 @@ public class OpenSSHKnownHosts
}
public interface HostEntry {
public interface KnownHostEntry {
KeyType getType();
String getFingerprint();
boolean appliesTo(String host)
throws IOException;
boolean appliesTo(String host) throws IOException;
boolean appliesTo(KeyType type, String host)
throws IOException;
boolean appliesTo(KeyType type, String host) throws IOException;
boolean verify(PublicKey key)
throws IOException;
boolean verify(PublicKey key) throws IOException;
String getLine();
}
public static class CommentEntry
implements HostEntry {
implements KnownHostEntry {
private final String comment;
public CommentEntry(String comment) {
@@ -290,8 +282,7 @@ public class OpenSSHKnownHosts
}
@Override
public boolean appliesTo(String host)
throws IOException {
public boolean appliesTo(String host) throws IOException {
return false;
}
@@ -311,17 +302,20 @@ public class OpenSSHKnownHosts
}
}
public static abstract class AbstractEntry
implements HostEntry {
public static class HostEntry implements KnownHostEntry {
protected final OpenSSHKnownHosts.Marker marker;
final OpenSSHKnownHosts.Marker marker;
private final String hostPart;
protected final KeyType type;
protected final PublicKey key;
private final KnownHostMatchers.HostMatcher matcher;
public AbstractEntry(Marker marker, KeyType type, PublicKey key) {
HostEntry(Marker marker, String hostPart, KeyType type, PublicKey key) throws SSHException {
this.marker = marker;
this.hostPart = hostPart;
this.type = type;
this.key = key;
this.matcher = KnownHostMatchers.createMatcher(hostPart);
}
@Override
@@ -335,8 +329,17 @@ public class OpenSSHKnownHosts
}
@Override
public boolean verify(PublicKey key)
throws IOException {
public boolean appliesTo(String host) throws IOException {
return matcher.match(host);
}
@Override
public boolean appliesTo(KeyType type, String host) throws IOException {
return this.type == type && matcher.match(host);
}
@Override
public boolean verify(PublicKey key) throws IOException {
return key.equals(this.key) && marker != Marker.REVOKED;
}
@@ -356,88 +359,8 @@ public class OpenSSHKnownHosts
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;
return hostPart;
}
}
@@ -456,12 +379,12 @@ public class OpenSSHKnownHosts
}
public static Marker fromString(String str) {
for (Marker m: values())
if (m.sMarker.equals(str))
for (Marker m: values()) {
if (m.sMarker.equals(str)) {
return m;
}
}
return null;
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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 com.hierynomus.sshj.transport.verification
import com.hierynomus.sshj.transport.verification.KnownHostMatchers
import spock.lang.Specification
import spock.lang.Unroll
class KnownHostMatchersSpec extends Specification {
@Unroll
def "should #yesno match host #host with pattern #pattern"() {
given:
def matcher = KnownHostMatchers.createMatcher(pattern)
expect:
match == matcher.match(host)
where:
pattern | host | match
"aaa.bbb.com" | "aaa.bbb.com" | true
"aaa.bbb.com" | "aaa.ccc.com" | false
"*.bbb.com" | "aaa.bbb.com" | true
"*.bbb.com" | "aaa.ccc.com" | false
"aaa.*.com" | "aaa.bbb.com" | true
"aaa.*.com" | "aaa.ccc.com" | true
"aaa.bbb.*" | "aaa.bbb.com" | true
"aaa.bbb.*" | "aaa.ccc.com" | false
"!*.bbb.com" | "aaa.bbb.com" | false
"!*.bbb.com" | "aaa.ccc.com" | true
"aaa.bbb.com,!*.ccc.com" | "xxx.yyy.com" | true
"aaa.bbb.com,!*.ccc.com" | "aaa.bbb.com" | true
"aaa.bbb.com,!*.ccc.com" | "aaa.ccc.com" | false
"aaa.b??.com" | "aaa.bbb.com" | true
"aaa.b??.com" | "aaa.bcd.com" | true
"aaa.b??.com" | "aaa.ccd.com" | false
"aaa.b??.com" | "aaa.bccd.com" | false
"|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg=" | "192.168.1.61" | true
"|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg=" | "192.168.2.61" | false
yesno = match ? "" : "no"
}
}