diff --git a/src/main/java/net/schmizz/sshj/SSHClient.java b/src/main/java/net/schmizz/sshj/SSHClient.java
index 469a15ad..378b24e8 100644
--- a/src/main/java/net/schmizz/sshj/SSHClient.java
+++ b/src/main/java/net/schmizz/sshj/SSHClient.java
@@ -38,6 +38,7 @@ import net.schmizz.sshj.transport.compression.DelayedZlibCompression;
import net.schmizz.sshj.transport.compression.NoneCompression;
import net.schmizz.sshj.transport.compression.ZlibCompression;
import net.schmizz.sshj.transport.verification.AlgorithmsVerifier;
+import net.schmizz.sshj.transport.verification.FingerprintVerifier;
import net.schmizz.sshj.transport.verification.HostKeyVerifier;
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
import net.schmizz.sshj.userauth.UserAuth;
@@ -169,19 +170,23 @@ public class SSHClient
/**
* Add a {@link HostKeyVerifier} that will verify any host that's able to claim a host key with the given {@code
- * fingerprint}, e.g. {@code "4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21"}
+ * fingerprint}.
+ *
+ * The fingerprint can be specified in either an MD5 colon-delimited format (16 hexadecimal octets, delimited by a colon),
+ * or in a Base64 encoded format for SHA-1 or SHA-256 fingerprints.
+ * Valid examples are:
+ *
+ *
- "SHA1:2Fo8c/96zv32xc8GZWbOGYOlRak="
+ * - "SHA256:oQGbQTujGeNIgh0ONthcEpA/BHxtt3rcYY+NxXTxQjs="
+ * - "MD5:d3:5e:40:72:db:08:f1:6d:0c:d7:6d:35:0d:ba:7c:32"
+ * - "d3:5e:40:72:db:08:f1:6d:0c:d7:6d:35:0d:ba:7c:32"
*
* @param fingerprint expected fingerprint in colon-delimited format (16 octets in hex delimited by a colon)
*
* @see SecurityUtils#getFingerprint
*/
public void addHostKeyVerifier(final String fingerprint) {
- addHostKeyVerifier(new HostKeyVerifier() {
- @Override
- public boolean verify(String h, int p, PublicKey k) {
- return SecurityUtils.getFingerprint(k).equals(fingerprint);
- }
- });
+ addHostKeyVerifier(FingerprintVerifier.getInstance(fingerprint));
}
// FIXME: there are way too many auth... overrides. Better API needed.
diff --git a/src/main/java/net/schmizz/sshj/transport/verification/FingerprintVerifier.java b/src/main/java/net/schmizz/sshj/transport/verification/FingerprintVerifier.java
new file mode 100644
index 00000000..24045ed8
--- /dev/null
+++ b/src/main/java/net/schmizz/sshj/transport/verification/FingerprintVerifier.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C)2009 - SSHJ Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.schmizz.sshj.transport.verification;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.regex.Pattern;
+
+import net.schmizz.sshj.common.Base64;
+import net.schmizz.sshj.common.Buffer;
+import net.schmizz.sshj.common.SSHRuntimeException;
+import net.schmizz.sshj.common.SecurityUtils;
+import net.schmizz.sshj.transport.verification.HostKeyVerifier;
+
+public class FingerprintVerifier implements HostKeyVerifier {
+ private static final Pattern MD5_FINGERPRINT_PATTERN = Pattern.compile("[0-9a-f]{2}+(:[0-9a-f]{2}+){15}+");
+ /**
+ * Valid examples:
+ *
+ *
+ * 4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21
+ * MD5:4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21
+ * SHA1:FghNYu1l/HyE/qWbdQ2mkxrd0rU
+ * SHA1:FghNYu1l/HyE/qWbdQ2mkxrd0rU=
+ * SHA256:l/SjyCoKP8jAx3d8k8MWH+UZG0gcuIR7TQRE/A3faQo
+ * SHA256:l/SjyCoKP8jAx3d8k8MWH+UZG0gcuIR7TQRE/A3faQo=
+ *
+ *
+ *
+ * @param fingerprint of an SSH fingerprint in MD5 (hex), SHA-1 (base64) or SHA-256(base64) format
+ *
+ * @return
+ */
+ public static HostKeyVerifier getInstance(String fingerprint) {
+
+ try {
+ if (fingerprint.startsWith("SHA1:")) {
+ return new FingerprintVerifier("SHA-1", fingerprint.substring(5));
+ }
+
+ if (fingerprint.startsWith("SHA256:")) {
+ return new FingerprintVerifier("SHA-256", fingerprint.substring(7));
+ }
+
+ final String md5;
+ if (fingerprint.startsWith("MD5:")) {
+ md5 = fingerprint.substring(4); // remove the MD5: prefix
+ } else {
+ md5 = fingerprint;
+ }
+
+ if (!MD5_FINGERPRINT_PATTERN.matcher(md5).matches()) {
+ throw new SSHRuntimeException("Invalid MD5 fingerprint: " + fingerprint);
+ }
+
+ // Use the old default fingerprint verifier for md5 fingerprints
+ return (new HostKeyVerifier() {
+ @Override
+ public boolean verify(String h, int p, PublicKey k) {
+ return SecurityUtils.getFingerprint(k).equals(md5);
+ }
+ });
+ } catch (SSHRuntimeException e) {
+ throw e;
+ } catch (IOException e) {
+ throw new SSHRuntimeException(e);
+ }
+
+ }
+
+ private final String digestAlgorithm;
+ private final byte[] fingerprintData;
+
+ /**
+ *
+ * @param digestAlgorithm
+ * the used digest algorithm
+ * @param base64Fingerprint
+ * base64 encoded fingerprint data
+ *
+ * @throws IOException
+ */
+ private FingerprintVerifier(String digestAlgorithm, String base64Fingerprint) throws IOException {
+ this.digestAlgorithm = digestAlgorithm;
+
+ // if the length is not padded with "=" chars at the end so that it is divisible by 4 the SSHJ Base64 implementation does not work correctly
+ StringBuilder base64FingerprintBuilder = new StringBuilder(base64Fingerprint);
+ while (base64FingerprintBuilder.length() % 4 != 0) {
+ base64FingerprintBuilder.append("=");
+ }
+ fingerprintData = Base64.decode(base64FingerprintBuilder.toString());
+ }
+
+ @Override
+ public boolean verify(String hostname, int port, PublicKey key) {
+ MessageDigest digest;
+ try {
+ digest = SecurityUtils.getMessageDigest(digestAlgorithm);
+ } catch (GeneralSecurityException e) {
+ throw new SSHRuntimeException(e);
+ }
+ digest.update(new Buffer.PlainBuffer().putPublicKey(key).getCompactData());
+
+ byte[] digestData = digest.digest();
+ return Arrays.equals(fingerprintData, digestData);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/groovy/net/schmizz/sshj/transport/verification/FingerprintVerifierSpec.groovy b/src/test/groovy/net/schmizz/sshj/transport/verification/FingerprintVerifierSpec.groovy
new file mode 100644
index 00000000..c7093c12
--- /dev/null
+++ b/src/test/groovy/net/schmizz/sshj/transport/verification/FingerprintVerifierSpec.groovy
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C)2009 - SSHJ Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.schmizz.sshj.transport.verification
+
+import net.schmizz.sshj.common.Base64
+import net.schmizz.sshj.common.Buffer
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class FingerprintVerifierSpec extends Specification {
+
+ @Unroll
+ def "should accept #digest fingerprints"() {
+ given:
+ def verifier = FingerprintVerifier.getInstance(fingerprint)
+ expect:
+ verifier.verify("", 0, getPublicKey())
+ where:
+ digest << ["SHA-1", "SHA-256", "MD5", "old style"]
+ fingerprint << ["SHA1:2Fo8c/96zv32xc8GZWbOGYOlRak=",
+ "SHA256:oQGbQTujGeNIgh0ONthcEpA/BHxtt3rcYY+NxXTxQjs=",
+ "MD5:d3:5e:40:72:db:08:f1:6d:0c:d7:6d:35:0d:ba:7c:32",
+ "d3:5e:40:72:db:08:f1:6d:0c:d7:6d:35:0d:ba:7c:32"]
+ }
+
+ @Unroll
+ def "should accept too short #digest fingerprints"() {
+ given:
+ def verifier = FingerprintVerifier.getInstance(fingerprint)
+ expect:
+ verifier.verify("", 0, getPublicKey())
+ where:
+ digest << ["SHA-1", "SHA-256"]
+ fingerprint << ["SHA1:2Fo8c/96zv32xc8GZWbOGYOlRak",
+ "SHA256:oQGbQTujGeNIgh0ONthcEpA/BHxtt3rcYY+NxXTxQjs"]
+
+ }
+
+
+ def getPublicKey() {
+ def lines = new File("src/test/resources/keytypes/test_ed25519.pub").readLines()
+ def keystring = lines[0].split(" ")[1]
+ return new Buffer.PlainBuffer(Base64.decode(keystring)).readPublicKey()
+ }
+}