Skip to content

Commit 2003a9f

Browse files
committed
Add support for verifying multiple host entries (Fixes #405)
1 parent 84a7677 commit 2003a9f

File tree

8 files changed

+191
-100
lines changed

8 files changed

+191
-100
lines changed

README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ Google Group: http://groups.google.com/group/sshj-users
107107
Fork away!
108108

109109
== Release history
110+
SSHJ 0.24.0 (2018-??-??)::
111+
* Added support for hmac-ripemd160
112+
110113
SSHJ 0.23.0 (2017-10-13)::
111114
* Merged https://github.com/hierynomus/sshj/pulls/372[#372]: Upgrade to 'net.i2p.crypto:eddsa:0.2.0'
112115
* Fixed https://github.com/hierynomus/sshj/issues/355[#355] and https://github.com/hierynomus/sshj/issues/354[#354]: Correctly decode signature bytes

src/main/java/net/schmizz/sshj/common/Buffer.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,10 +460,13 @@ public T putSensitiveString(char[] str) {
460460

461461
public PublicKey readPublicKey()
462462
throws BufferException {
463+
KeyType keyType = KeyType.fromString(readString());
463464
try {
464-
return KeyType.fromString(readString()).readPubKeyFromBuffer(this);
465+
return keyType.readPubKeyFromBuffer(this);
465466
} catch (GeneralSecurityException e) {
466467
throw new SSHRuntimeException(e);
468+
} catch (UnsupportedOperationException uoe) {
469+
throw new BufferException("Could not decode keytype " + keyType);
467470
}
468471
}
469472

src/main/java/net/schmizz/sshj/transport/verification/ConsoleKnownHostsVerifier.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public ConsoleKnownHostsVerifier(File khFile, Console console)
4141
protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) {
4242
final KeyType type = KeyType.fromKey(key);
4343
console.printf("The authenticity of host '%s' can't be established.\n" +
44-
"%s key fingerprint is %s.\n", hostname, type, SecurityUtils.getFingerprint(key));
44+
"%s key fingerprint is %s.\n", hostname, type, SecurityUtils.getFingerprint(key));
4545
String response = console.readLine("Are you sure you want to continue connecting (yes/no)? ");
4646
while (!(response.equalsIgnoreCase(YES) || response.equalsIgnoreCase(NO))) {
4747
response = console.readLine("Please explicitly enter yes/no: ");
@@ -60,7 +60,7 @@ protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) {
6060
}
6161

6262
@Override
63-
protected boolean hostKeyChangedAction(KnownHostEntry entry, String hostname, PublicKey key) {
63+
protected boolean hostKeyChangedAction(String hostname, PublicKey key) {
6464
final KeyType type = KeyType.fromKey(key);
6565
final String fp = SecurityUtils.getFingerprint(key);
6666
final String path = getFile().getAbsolutePath();

src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,23 @@ public boolean verify(final String hostname, final int port, final PublicKey key
8787

8888
final String adjustedHostname = (port != 22) ? "[" + hostname + "]:" + port : hostname;
8989

90+
boolean foundApplicableHostEntry = false;
9091
for (KnownHostEntry e : entries) {
9192
try {
92-
if (e.appliesTo(type, adjustedHostname))
93-
return e.verify(key) || hostKeyChangedAction(e, adjustedHostname, key);
93+
if (e.appliesTo(type, adjustedHostname)) {
94+
foundApplicableHostEntry = true;
95+
if (e.verify(key)) {
96+
return true;
97+
}
98+
}
9499
} catch (IOException ioe) {
95100
log.error("Error with {}: {}", e, ioe);
96101
return false;
97102
}
103+
104+
}
105+
if (foundApplicableHostEntry) {
106+
return hostKeyChangedAction(adjustedHostname, key);
98107
}
99108

100109
return hostKeyUnverifiableAction(adjustedHostname, key);
@@ -104,7 +113,7 @@ protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) {
104113
return false;
105114
}
106115

107-
protected boolean hostKeyChangedAction(KnownHostEntry entry, String hostname, PublicKey key) {
116+
protected boolean hostKeyChangedAction(String hostname, PublicKey key) {
108117
log.warn("Host key for `{}` has changed!", hostname);
109118
return false;
110119
}
@@ -199,7 +208,7 @@ public KnownHostEntry parseEntry(String line)
199208
}
200209
if(split.length < 3) {
201210
log.error("Error reading entry `{}`", line);
202-
return null;
211+
return new BadHostEntry(line);
203212
}
204213
final String hostnames = split[i++];
205214
final String sType = split[i++];
@@ -209,7 +218,13 @@ public KnownHostEntry parseEntry(String line)
209218

210219
if (type != KeyType.UNKNOWN) {
211220
final String sKey = split[i++];
212-
key = new Buffer.PlainBuffer(Base64.decode(sKey)).readPublicKey();
221+
try {
222+
byte[] keyBytes = Base64.decode(sKey);
223+
key = new Buffer.PlainBuffer(keyBytes).readPublicKey();
224+
} catch (IOException ioe) {
225+
log.warn("Error decoding Base64 key bytes", ioe);
226+
return new BadHostEntry(line);
227+
}
213228
} else if (isBits(sType)) {
214229
type = KeyType.RSA;
215230
// int bits = Integer.valueOf(sType);
@@ -220,11 +235,11 @@ public KnownHostEntry parseEntry(String line)
220235
key = keyFactory.generatePublic(new RSAPublicKeySpec(n, e));
221236
} catch (Exception ex) {
222237
log.error("Error reading entry `{}`, could not create key", line, ex);
223-
return null;
238+
return new BadHostEntry(line);
224239
}
225240
} else {
226241
log.error("Error reading entry `{}`, could not determine type", line);
227-
return null;
242+
return new BadHostEntry(line);
228243
}
229244

230245
return new HostEntry(marker, hostnames, type, key);
@@ -364,6 +379,44 @@ protected String getHostPart() {
364379
}
365380
}
366381

382+
public static class BadHostEntry implements KnownHostEntry {
383+
private String line;
384+
385+
public BadHostEntry(String line) {
386+
this.line = line;
387+
}
388+
389+
@Override
390+
public KeyType getType() {
391+
return KeyType.UNKNOWN;
392+
}
393+
394+
@Override
395+
public String getFingerprint() {
396+
return null;
397+
}
398+
399+
@Override
400+
public boolean appliesTo(String host) throws IOException {
401+
return false;
402+
}
403+
404+
@Override
405+
public boolean appliesTo(KeyType type, String host) throws IOException {
406+
return false;
407+
}
408+
409+
@Override
410+
public boolean verify(PublicKey key) throws IOException {
411+
return false;
412+
}
413+
414+
@Override
415+
public String getLine() {
416+
return line;
417+
}
418+
}
419+
367420
public enum Marker {
368421
CA_CERT("@cert-authority"),
369422
REVOKED("@revoked");
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright (C)2009 - SSHJ Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.hierynomus.sshj.transport.verification
17+
18+
import net.schmizz.sshj.common.Base64
19+
import net.schmizz.sshj.common.Buffer
20+
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts
21+
import org.junit.Rule
22+
import org.junit.rules.TemporaryFolder
23+
import spock.lang.Specification
24+
import spock.lang.Unroll
25+
26+
import static org.hamcrest.CoreMatchers.equalTo
27+
import static org.hamcrest.CoreMatchers.instanceOf
28+
import static org.junit.Assert.assertThat
29+
import static org.junit.Assert.assertThat
30+
31+
class OpenSSHKnownHostsSpec extends Specification {
32+
33+
@Rule
34+
TemporaryFolder folder
35+
36+
def "should check all host entries for key"() {
37+
given:
38+
def f = knownHosts("""
39+
host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCiYp2IDgzDFhl8T4TRLIhEljvEixz1YN0XWh4dYh0REGK9T4QKiyb28EztPMdcOtz1uyX5rUGYXX9hj99S4SiU=
40+
host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck=
41+
""")
42+
def pk = new Buffer.PlainBuffer(Base64.decode("AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck=")).readPublicKey()
43+
when:
44+
def knownhosts = new OpenSSHKnownHosts(f)
45+
46+
then:
47+
knownhosts.verify("host1", 22, pk)
48+
}
49+
50+
def "should not fail on bad base64 entry"() {
51+
given:
52+
def f = knownHosts("""
53+
host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTIDgzDFhl8T4TRLIhEljvEixz1YN0XWh4dYh0REGK9T4QKiyb28EztPMdcOtz1uyX5rUGYXX9hj99S4SiU=
54+
host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck=
55+
""")
56+
def pk = new Buffer.PlainBuffer(Base64.decode("AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck=")).readPublicKey()
57+
when:
58+
def knownhosts = new OpenSSHKnownHosts(f)
59+
60+
then:
61+
knownhosts.verify("host1", 22, pk)
62+
}
63+
64+
def "should mark bad line and not fail"() {
65+
given:
66+
def f = knownHosts("M36Lo+Ik5ukNugvvoNFlpnyiHMmtKxt3FpyEfYuryXjNqMNWHn/ARVnpUIl5jRLTB7WBzyLYMG7X5nuoFL9zYqKGtHxChbDunxMVbspw5WXI9VN+qxcLwmITmpEvI9ApyS/Ox2ZyN7zw==\n")
67+
68+
when:
69+
def knownhosts = new OpenSSHKnownHosts(f)
70+
71+
then:
72+
knownhosts.entries().size() == 1
73+
knownhosts.entries().get(0) instanceof OpenSSHKnownHosts.BadHostEntry
74+
}
75+
76+
@Unroll
77+
def "should add comment for #type line"() {
78+
given:
79+
def f = knownHosts(s)
80+
81+
when:
82+
def knownHosts = new OpenSSHKnownHosts(f)
83+
84+
then:
85+
knownHosts.entries().size() == 1
86+
knownHosts.entries().get(0) instanceof OpenSSHKnownHosts.CommentEntry
87+
88+
where:
89+
type << ["empty", "comment"]
90+
s << ["\n", "#comment\n"]
91+
}
92+
93+
@Unroll
94+
def "should match any host name from multi-host line"() {
95+
given:
96+
def f = knownHosts("schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==")
97+
def pk = new Buffer.PlainBuffer(Base64.decode("AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==")).readPublicKey()
98+
99+
when:
100+
def knownHosts = new OpenSSHKnownHosts(f)
101+
102+
then:
103+
knownHosts.verify(h, 22, pk)
104+
105+
where:
106+
h << ["schmizz.net", "69.163.155.180"]
107+
}
108+
109+
def knownHosts(String s) {
110+
def f = folder.newFile("known_hosts")
111+
f.write(s)
112+
return f
113+
}
114+
}

src/test/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHostsTest.java

Lines changed: 0 additions & 88 deletions
This file was deleted.

src/test/resources/known_hosts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
# Line 1: bogus line, with invalid base64 string
2+
# Line 2: syntactically correct line, but incorrect base64 string
3+
# Line 3: correct line (that should match) Above we have a plain line
4+
# Line 4: hashed line
5+
# Line 5: v1 line
6+
# Comment lines like these should be ignored
7+
schmizz.net,69.163.155.180 ssh-rsa #BOGUS#aC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==
8+
schmizz.net,69.163.155.180 ssh-rsa AAAAAAAAAC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==
19
schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==
2-
# Above we have a plain line, Below we have a hashed line, Last is a v1 line, This is a garbage line.
310
|1|dy7xSefq6NmJms6AzANG3w45W28=|SSCTlHs4pZbc2uaRoPvjyEAHE1g= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAu64GJcCkdtckPGt8uKTyhG1ShT1Np1kh10eE49imQ4Nh9Y/IrSPzDtYUAazQ88ABc2NffuOKkdn2qtUwZ1ulfcdNfN3oTim3BiVHqa041pKG0L+onQe8Bo+CaG5KBLy/C24eNGM9EcfQvDQOnq1eD3lnR/l8fFckldzjfxZgar0yT9Bb3pwp50oN+1wSEINJEHOgMIW8kZBQmyNr/B+b7yX+Y1s1vuYIP/i4WimCVmkdi9G87Ga8w7GxKalRD2QOG6Xms2YWRQDN6M/MOn4tda3EKolbWkctEWcQf/PcVJffTH4Wv5f0RjVyrQv4ha4FZcNAv6RkRd9WkiCsiTKioQ==
411
test.com,1.1.1.1 2048 35 22017496617994656680820635966392838863613340434802393112245951008866692373218840197754553998457793202561151141246686162285550121243768846314646395880632789308110750881198697743542374668273149584280424505890648953477691795864456749782348425425954366277600319096366690719901119774784695056100331902394094537054256611668966698242432417382422091372756244612839068092471592121759862971414741954991375710930168229171638843329213652899594987626853020377726482288618521941129157643483558764875338089684351824791983007780922947554898825663693324944982594850256042689880090306493029526546183035567296830604572253312294059766327

src/test/resources/known_hosts.invalid

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)