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>tags/v5.5.0.201909041048-rc1
@@ -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)", |
@@ -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 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 |
@@ -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(); |
@@ -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); |
@@ -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. | |||
* |
@@ -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, // |
@@ -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"; | |||