From 2d34d0bd9c6e5bad80befd42b76d5658de8e0d4d Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Fri, 21 Jun 2019 22:39:19 +0200 Subject: [PATCH] sshd: support the HashKnownHosts configuration Add the constant, and implement hashing of known host names in OpenSshServerKeyDatabase. Add a test verifying that the hashing works. Bug: 548492 Change-Id: Iabe82b666da627bd7f4d82519a366d166aa9ddd4 Signed-off-by: Thomas Wolf --- .../META-INF/MANIFEST.MF | 3 +- .../jgit/transport/sshd/ApacheSshTest.java | 53 ++++++++++++++++++- .../transport/sshd/JGitServerKeyVerifier.java | 8 +++ .../sshd/OpenSshServerKeyDatabase.java | 48 ++++++++++++----- .../transport/sshd/ServerKeyDatabase.java | 8 +++ .../jgit/transport/ssh/SshTestBase.java | 2 +- .../eclipse/jgit/transport/SshConstants.java | 7 +++ 7 files changed, 114 insertions(+), 15 deletions(-) diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF index af0ef2f21..d0383b82e 100644 --- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF @@ -7,7 +7,8 @@ Bundle-Version: 5.5.0.qualifier Bundle-Vendor: %Bundle-Vendor Bundle-Localization: plugin Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Import-Package: org.apache.sshd.common;version="[2.2.0,2.3.0)", +Import-Package: org.apache.sshd.client.config.hosts;version="[2.2.0,2.3.0)", + org.apache.sshd.common;version="[2.2.0,2.3.0)", org.apache.sshd.common.auth;version="[2.2.0,2.3.0)", org.apache.sshd.common.config.keys;version="[2.2.0,2.3.0)", org.apache.sshd.common.keyprovider;version="[2.2.0,2.3.0)", diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java index df0b832a0..b9b7353d3 100644 --- a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java +++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java @@ -42,16 +42,22 @@ */ package org.eclipse.jgit.transport.sshd; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.sshd.client.config.hosts.KnownHostEntry; import org.eclipse.jgit.api.errors.TransportException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.ssh.SshTestBase; -import org.eclipse.jgit.transport.sshd.SshdSessionFactory; import org.eclipse.jgit.util.FS; import org.junit.Test; import org.junit.experimental.theories.Theories; @@ -101,6 +107,51 @@ public void testEd25519HostKey() throws Exception { "IdentityFile " + privateKey1.getAbsolutePath()); } + @Test + public void testHashedKnownHosts() throws Exception { + assertTrue("Failed to delete known_hosts", knownHosts.delete()); + // The provider will answer "yes" to all questions, so we should be able + // to connect and end up with a new known_hosts file with the host key. + TestCredentialsProvider provider = new TestCredentialsProvider(); + cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, // + "HashKnownHosts yes", // + "Host localhost", // + "HostName localhost", // + "Port " + testPort, // + "User " + TEST_USER, // + "IdentityFile " + privateKey1.getAbsolutePath()); + List messages = provider.getLog(); + assertFalse("Expected user interaction", messages.isEmpty()); + assertEquals( + "Expected to be asked about the key, and the file creation", 2, + messages.size()); + assertTrue("~/.ssh/known_hosts should exist now", knownHosts.exists()); + // Let's clone again without provider. If it works, the server host key + // was written correctly. + File clonedAgain = new File(getTemporaryDirectory(), "cloned2"); + cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, // + "Host localhost", // + "HostName localhost", // + "Port " + testPort, // + "User " + TEST_USER, // + "IdentityFile " + privateKey1.getAbsolutePath()); + // Check that the first line contains neither "localhost" nor + // "127.0.0.1", but does contain the expected hash. + List lines = Files.readAllLines(knownHosts.toPath()).stream() + .filter(s -> s != null && s.length() >= 1 && s.charAt(0) != '#' + && !s.trim().isEmpty()) + .collect(Collectors.toList()); + assertEquals("Unexpected number of known_hosts lines", 1, lines.size()); + String line = lines.get(0); + assertFalse("Found host in line", line.contains("localhost")); + assertFalse("Found IP in line", line.contains("127.0.0.1")); + assertTrue("Hash not found", line.contains("|")); + KnownHostEntry entry = KnownHostEntry.parseKnownHostEntry(line); + assertTrue("Hash doesn't match localhost", + entry.isHostMatch("localhost", testPort) + || entry.isHostMatch("127.0.0.1", testPort)); + } + @Test public void testPreamble() throws Exception { // Test that the client can deal with strange lines being sent before diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java index c1d10bd44..b94515c15 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java @@ -42,6 +42,8 @@ */ package org.eclipse.jgit.internal.transport.sshd; +import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.flag; + import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.PublicKey; @@ -174,6 +176,12 @@ public StrictHostKeyChecking getStrictHostKeyChecking() { } } + @Override + public boolean getHashKnownHosts() { + HostConfigEntry entry = session.getHostConfigEntry(); + return flag(entry.getProperty(SshConstants.HASH_KNOWN_HOSTS)); + } + @Override public String getUsername() { return session.getUsername(); diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java index e43310968..f4849ce4a 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java @@ -58,6 +58,7 @@ import java.nio.file.Paths; import java.security.GeneralSecurityException; import java.security.PublicKey; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -70,15 +71,18 @@ import java.util.function.Supplier; import org.apache.sshd.client.config.hosts.HostPatternsHolder; +import org.apache.sshd.client.config.hosts.KnownHostDigest; import org.apache.sshd.client.config.hosts.KnownHostEntry; import org.apache.sshd.client.config.hosts.KnownHostHashValue; import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair; import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.config.keys.PublicKeyEntry; import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; import org.apache.sshd.common.digest.BuiltinDigests; +import org.apache.sshd.common.mac.Mac; import org.apache.sshd.common.util.io.ModifiableFileWatcher; import org.apache.sshd.common.util.net.SshdSocketAddress; import org.eclipse.jgit.annotations.NonNull; @@ -276,12 +280,13 @@ public boolean accept(@NonNull String connectAddress, try { if (Files.exists(path) || !askAboutNewFile || ask.createNewFile(path)) { - updateKnownHostsFile(candidates, serverKey, path); + updateKnownHostsFile(candidates, serverKey, path, + config); toUpdate.resetReloadAttributes(); } - } catch (IOException e) { + } catch (Exception e) { LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate, - path)); + path), e); } } return true; @@ -342,9 +347,9 @@ private List addUserHostKeyFiles(List fileNames) { } private void updateKnownHostsFile(Collection candidates, - PublicKey serverKey, Path path) - throws IOException { - String newEntry = createHostKeyLine(candidates, serverKey); + PublicKey serverKey, Path path, Configuration config) + throws Exception { + String newEntry = createHostKeyLine(candidates, serverKey, config); if (newEntry == null) { return; } @@ -703,14 +708,33 @@ private Collection getCandidates( } private String createHostKeyLine(Collection patterns, - PublicKey key) throws IOException { + PublicKey key, Configuration config) throws Exception { StringBuilder result = new StringBuilder(); - for (SshdSocketAddress address : patterns) { - if (result.length() > 0) { - result.append(','); + if (config.getHashKnownHosts()) { + // SHA1 is the only algorithm for host name hashing known to OpenSSH + // or to Apache MINA sshd. + NamedFactory digester = KnownHostDigest.SHA1; + Mac mac = digester.create(); + SecureRandom prng = new SecureRandom(); + byte[] salt = new byte[mac.getDefaultBlockSize()]; + for (SshdSocketAddress address : patterns) { + if (result.length() > 0) { + result.append(','); + } + prng.nextBytes(salt); + KnownHostHashValue.append(result, digester, salt, + KnownHostHashValue.calculateHashValue( + address.getHostName(), address.getPort(), mac, + salt)); + } + } else { + for (SshdSocketAddress address : patterns) { + if (result.length() > 0) { + result.append(','); + } + KnownHostHashValue.appendHostPattern(result, + address.getHostName(), address.getPort()); } - KnownHostHashValue.appendHostPattern(result, address.getHostName(), - address.getPort()); } result.append(' '); PublicKeyEntry.appendPublicKeyEntry(result, key); diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java index ce58d9518..bdfb96d0c 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java @@ -158,6 +158,14 @@ enum StrictHostKeyChecking { @NonNull StrictHostKeyChecking getStrictHostKeyChecking(); + /** + * Obtains the value of the "HashKnownHosts" ssh config. + * + * @return {@code true} if new entries should be stored with hashed host + * information, {@code false} otherwise + */ + boolean getHashKnownHosts(); + /** * Obtains the user name used in the connection attempt. * diff --git a/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java b/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java index b8c90b2a4..dab93fd07 100644 --- a/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java +++ b/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java @@ -305,7 +305,7 @@ public void testSshWithoutKnownHostsWithProviderAsk() // without provider. If it works, the server host key was written // correctly. File clonedAgain = new File(getTemporaryDirectory(), "cloned2"); - cloneWith("ssh://localhost/doesntmatter", clonedAgain, provider, // + cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, // "Host localhost", // "HostName localhost", // "Port " + testPort, // diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java index 2b79d7105..efbe77704 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java @@ -101,6 +101,13 @@ private SshConstants() { /** Key in an ssh config file. */ public static final String GLOBAL_KNOWN_HOSTS_FILE = "GlobalKnownHostsFile"; + /** + * Key in an ssh config file. + * + * @since 5.5 + */ + public static final String HASH_KNOWN_HOSTS = "HashKnownHosts"; + /** Key in an ssh config file. */ public static final String HOST = "Host";