mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 07:10:53 +03:00
Added support for wildcard host entries in known_hosts (Fixes #331)
This commit is contained in:
@@ -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 + ']';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user