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>
This commit is contained in:
parent
124fbbc33a
commit
2d34d0bd9c
|
@ -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)",
|
||||||
|
|
|
@ -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 void testEd25519HostKey() throws Exception {
|
||||||
"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
|
||||||
|
|
|
@ -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 StrictHostKeyChecking getStrictHostKeyChecking() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
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.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 boolean accept(@NonNull String connectAddress,
|
||||||
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 @@ private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 @@ private Collection<SshdSocketAddress> getCandidates(
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
for (SshdSocketAddress address : patterns) {
|
if (config.getHashKnownHosts()) {
|
||||||
if (result.length() > 0) {
|
// SHA1 is the only algorithm for host name hashing known to OpenSSH
|
||||||
result.append(',');
|
// 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(' ');
|
result.append(' ');
|
||||||
PublicKeyEntry.appendPublicKeyEntry(result, key);
|
PublicKeyEntry.appendPublicKeyEntry(result, key);
|
||||||
|
|
|
@ -158,6 +158,14 @@ enum StrictHostKeyChecking {
|
||||||
@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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -305,7 +305,7 @@ public void testSshWithoutKnownHostsWithProviderAsk()
|
||||||
// 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, //
|
||||||
|
|
|
@ -101,6 +101,13 @@ private 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…
Reference in New Issue