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: + * + * * * @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: + * + * + * + * + * @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() + } +}