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)",
*/
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;
"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
*/
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;
}
}
+ @Override
+ public boolean getHashKnownHosts() {
+ HostConfigEntry entry = session.getHostConfigEntry();
+ return flag(entry.getProperty(SshConstants.HASH_KNOWN_HOSTS));
+ }
+
@Override
public String getUsername() {
return session.getUsername();
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;
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;
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;
}
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;
}
}
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);
@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.
*
// 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, //
/** 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";