]> source.dussan.org Git - jgit.git/commitdiff
sshd: support the HashKnownHosts configuration 65/144665/7
authorThomas Wolf <thomas.wolf@paranor.ch>
Fri, 21 Jun 2019 20:39:19 +0000 (22:39 +0200)
committerThomas Wolf <thomas.wolf@paranor.ch>
Mon, 2 Sep 2019 19:30:27 +0000 (21:30 +0200)
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 <thomas.wolf@paranor.ch>
org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java
org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java

index af0ef2f219e3ea6c11b71d3ca6f18be2903da7f2..d0383b82e50d2e359328e8a927b3c8ce892e3020 100644 (file)
@@ -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)",
index df0b832a0fd81a06ac426a2b7cb4b86fe807ff18..b9b7353d3e8e90b2f8ad80fc1f525b056364b772 100644 (file)
  */
 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 class ApacheSshTest extends SshTestBase {
                                "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<LogEntry> 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<String> 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
index c1d10bd44accfeb399433bab51ea1f4d599bd2d1..b94515c157994ee080f2957aacf5beb915321f21 100644 (file)
@@ -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 class JGitServerKeyVerifier
                        }
                }
 
+               @Override
+               public boolean getHashKnownHosts() {
+                       HostConfigEntry entry = session.getHostConfigEntry();
+                       return flag(entry.getProperty(SshConstants.HASH_KNOWN_HOSTS));
+               }
+
                @Override
                public String getUsername() {
                        return session.getUsername();
index e433109687ac19ec7edcc044622267240156f739..f4849ce4a39a2aeb5defc693b8cc064fb9eeeee0 100644 (file)
@@ -58,6 +58,7 @@ import java.nio.file.Path;
 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.concurrent.ConcurrentHashMap;
 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 class OpenSshServerKeyDatabase
                                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 @@ public class OpenSshServerKeyDatabase
        }
 
        private void updateKnownHostsFile(Collection<SshdSocketAddress> 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 @@ public class OpenSshServerKeyDatabase
        }
 
        private String createHostKeyLine(Collection<SshdSocketAddress> 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<Mac> 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);
index ce58d9518b9e74072ebdd54d622f72e8b8e910d4..bdfb96d0c74bb0672d3606455e3de23ab18197bf 100644 (file)
@@ -158,6 +158,14 @@ public interface ServerKeyDatabase {
                @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.
                 *
index b8c90b2a4065a704fb37fb893b50a37e1c12cf33..dab93fd07138cf70d9c0d0da9ea8b8e582e2899d 100644 (file)
@@ -305,7 +305,7 @@ public abstract class SshTestBase extends SshTestHarness {
                // 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, //
index 2b79d7105c4e197b2577b37d4b966927b18cfe0f..efbe77704b427d7419d42b2226f530d716124db6 100644 (file)
@@ -101,6 +101,13 @@ public final class 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";