Browse Source

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 <thomas.wolf@paranor.ch>
stable-5.5
Thomas Wolf 6 years ago
parent
commit
2d34d0bd9c
  1. 3
      org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
  2. 53
      org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
  3. 8
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java
  4. 42
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java
  5. 8
      org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java
  6. 2
      org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java
  7. 7
      org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java

3
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-Vendor: %Bundle-Vendor
Bundle-Localization: plugin Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: JavaSE-1.8 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.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.config.keys;version="[2.2.0,2.3.0)",
org.apache.sshd.common.keyprovider;version="[2.2.0,2.3.0)", org.apache.sshd.common.keyprovider;version="[2.2.0,2.3.0)",

53
org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java

@ -42,16 +42,22 @@
*/ */
package org.eclipse.jgit.transport.sshd; 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.File;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.Arrays; 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.api.errors.TransportException;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.ssh.SshTestBase; import org.eclipse.jgit.transport.ssh.SshTestBase;
import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
import org.junit.Test; import org.junit.Test;
import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theories;
@ -101,6 +107,51 @@ public class ApacheSshTest extends SshTestBase {
"IdentityFile " + privateKey1.getAbsolutePath()); "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 @Test
public void testPreamble() throws Exception { public void testPreamble() throws Exception {
// Test that the client can deal with strange lines being sent before // Test that the client can deal with strange lines being sent before

8
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; package org.eclipse.jgit.internal.transport.sshd;
import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.flag;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.security.PublicKey; 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 @Override
public String getUsername() { public String getUsername() {
return session.getUsername(); return session.getUsername();

42
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java

@ -58,6 +58,7 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -70,15 +71,18 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.apache.sshd.client.config.hosts.HostPatternsHolder; 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.KnownHostEntry;
import org.apache.sshd.client.config.hosts.KnownHostHashValue; import org.apache.sshd.client.config.hosts.KnownHostHashValue;
import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair; import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
import org.apache.sshd.client.session.ClientSession; 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.AuthorizedKeyEntry;
import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.PublicKeyEntry; import org.apache.sshd.common.config.keys.PublicKeyEntry;
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
import org.apache.sshd.common.digest.BuiltinDigests; 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.io.ModifiableFileWatcher;
import org.apache.sshd.common.util.net.SshdSocketAddress; import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.NonNull;
@ -276,12 +280,13 @@ public class OpenSshServerKeyDatabase
try { try {
if (Files.exists(path) || !askAboutNewFile if (Files.exists(path) || !askAboutNewFile
|| ask.createNewFile(path)) { || ask.createNewFile(path)) {
updateKnownHostsFile(candidates, serverKey, path); updateKnownHostsFile(candidates, serverKey, path,
config);
toUpdate.resetReloadAttributes(); toUpdate.resetReloadAttributes();
} }
} catch (IOException e) { } catch (Exception e) {
LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate, LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
path)); path), e);
} }
} }
return true; return true;
@ -342,9 +347,9 @@ public class OpenSshServerKeyDatabase
} }
private void updateKnownHostsFile(Collection<SshdSocketAddress> candidates, private void updateKnownHostsFile(Collection<SshdSocketAddress> candidates,
PublicKey serverKey, Path path) PublicKey serverKey, Path path, Configuration config)
throws IOException { throws Exception {
String newEntry = createHostKeyLine(candidates, serverKey); String newEntry = createHostKeyLine(candidates, serverKey, config);
if (newEntry == null) { if (newEntry == null) {
return; return;
} }
@ -703,14 +708,33 @@ public class OpenSshServerKeyDatabase
} }
private String createHostKeyLine(Collection<SshdSocketAddress> patterns, private String createHostKeyLine(Collection<SshdSocketAddress> patterns,
PublicKey key) throws IOException { PublicKey key, Configuration config) throws Exception {
StringBuilder result = new StringBuilder(); StringBuilder result = new StringBuilder();
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) { for (SshdSocketAddress address : patterns) {
if (result.length() > 0) { if (result.length() > 0) {
result.append(','); result.append(',');
} }
KnownHostHashValue.appendHostPattern(result, address.getHostName(), KnownHostHashValue.appendHostPattern(result,
address.getPort()); address.getHostName(), address.getPort());
}
} }
result.append(' '); result.append(' ');
PublicKeyEntry.appendPublicKeyEntry(result, key); PublicKeyEntry.appendPublicKeyEntry(result, key);

8
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java

@ -158,6 +158,14 @@ public interface ServerKeyDatabase {
@NonNull @NonNull
StrictHostKeyChecking getStrictHostKeyChecking(); 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. * Obtains the user name used in the connection attempt.
* *

2
org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java

@ -305,7 +305,7 @@ public abstract class SshTestBase extends SshTestHarness {
// without provider. If it works, the server host key was written // without provider. If it works, the server host key was written
// correctly. // correctly.
File clonedAgain = new File(getTemporaryDirectory(), "cloned2"); File clonedAgain = new File(getTemporaryDirectory(), "cloned2");
cloneWith("ssh://localhost/doesntmatter", clonedAgain, provider, // cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, //
"Host localhost", // "Host localhost", //
"HostName localhost", // "HostName localhost", //
"Port " + testPort, // "Port " + testPort, //

7
org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java

@ -101,6 +101,13 @@ public final class SshConstants {
/** Key in an ssh config file. */ /** Key in an ssh config file. */
public static final String GLOBAL_KNOWN_HOSTS_FILE = "GlobalKnownHostsFile"; 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. */ /** Key in an ssh config file. */
public static final String HOST = "Host"; public static final String HOST = "Host";

Loading…
Cancel
Save