From 124fbbc33a05c177767c5f4233717765acb1ab4d Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Thu, 20 Jun 2019 19:43:49 +0200 Subject: [PATCH] sshd: configurable server key verification Provide a wrapper interface and change the implementation such that a client can substitute its own database of known hosts keys instead of the default file-based mechanism. Bug: 547619 Change-Id: Ifc25a4519fa5bcf7bb8541b9f3e2de15215e3d66 Signed-off-by: Thomas Wolf --- .../META-INF/MANIFEST.MF | 9 +- .../jgit/transport/sshd/NoFilesSshTest.java | 221 ++++++++++++++++++ .../transport/sshd/JGitServerKeyVerifier.java | 182 +++++++++++++++ ...ier.java => OpenSshServerKeyDatabase.java} | 161 +++++++------ .../transport/sshd/ServerKeyDatabase.java | 169 ++++++++++++++ .../transport/sshd/SshdSessionFactory.java | 35 +-- 6 files changed, 688 insertions(+), 89 deletions(-) create mode 100644 org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java create mode 100644 org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java rename org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/{OpenSshServerKeyVerifier.java => OpenSshServerKeyDatabase.java} (86%) create mode 100644 org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF index 504a20eae..af0ef2f21 100644 --- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF @@ -7,7 +7,14 @@ Bundle-Version: 5.5.0.qualifier Bundle-Vendor: %Bundle-Vendor Bundle-Localization: plugin Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Import-Package: org.eclipse.jgit.api.errors;version="[5.5.0,5.6.0)", +Import-Package: 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)", + org.apache.sshd.common.session;version="[2.2.0,2.3.0)", + org.apache.sshd.common.util.net;version="[2.2.0,2.3.0)", + org.apache.sshd.common.util.security;version="[2.2.0,2.3.0)", + org.eclipse.jgit.api.errors;version="[5.5.0,5.6.0)", org.eclipse.jgit.internal.transport.sshd.proxy;version="[5.5.0,5.6.0)", org.eclipse.jgit.junit;version="[5.5.0,5.6.0)", org.eclipse.jgit.junit.ssh;version="[5.5.0,5.6.0)", diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java new file mode 100644 index 000000000..c0dc2cf20 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2019 Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport.sshd; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.KeyIdentityProvider; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.ssh.SshTestHarness; +import org.eclipse.jgit.util.FS; +import org.junit.After; +import org.junit.Test; + +/** + * Test for using the SshdSessionFactory without files in ~/.ssh but with an + * in-memory setup. + */ +public class NoFilesSshTest extends SshTestHarness { + + + private PublicKey testServerKey; + + private KeyPair testUserKey; + + @Override + protected SshSessionFactory createSessionFactory() { + SshdSessionFactory result = new SshdSessionFactory(new JGitKeyCache(), + null) { + + @Override + protected File getSshConfig(File dir) { + return null; + } + + @Override + protected ServerKeyDatabase getServerKeyDatabase(File homeDir, + File dir) { + return new ServerKeyDatabase() { + + @Override + public List lookup(String connectAddress, + InetSocketAddress remoteAddress, + Configuration config) { + return Collections.singletonList(testServerKey); + } + + @Override + public boolean accept(String connectAddress, + InetSocketAddress remoteAddress, + PublicKey serverKey, Configuration config, + CredentialsProvider provider) { + return KeyUtils.compareKeys(serverKey, testServerKey); + } + + }; + } + + @Override + protected Iterable getDefaultKeys(File dir) { + // This would work for this simple test case: + // return Collections.singletonList(testUserKey); + // But let's see if we can check the host and username that's used. + // For that, we need access to the sshd SessionContext: + return new KeyAuthenticator(); + } + + @Override + protected String getDefaultPreferredAuthentications() { + return "publickey"; + } + }; + + // The home directory is mocked at this point! + result.setHomeDirectory(FS.DETECTED.userHome()); + result.setSshDirectory(sshDir); + return result; + } + + private class KeyAuthenticator implements KeyIdentityProvider, Iterable { + + @Override + public Iterator iterator() { + // Should not be called. The use of the Iterable interface in + // SshdSessionFactory.getDefaultKeys() made sense in sshd 2.0.0, + // but sshd 2.2.0 added the SessionContext, which although good + // (without it we couldn't check here) breaks the Iterable analogy. + // But we're stuck now with that interface for getDefaultKeys, and + // so this override throwing an exception is unfortunately needed. + throw new UnsupportedOperationException(); + } + + @Override + public Iterable loadKeys(SessionContext session) + throws IOException, GeneralSecurityException { + if (!TEST_USER.equals(session.getUsername())) { + return Collections.emptyList(); + } + SshdSocketAddress remoteAddress = SshdSocketAddress + .toSshdSocketAddress(session.getRemoteAddress()); + switch (remoteAddress.getHostName()) { + case "localhost": + case "127.0.0.1": + return Collections.singletonList(testUserKey); + default: + return Collections.emptyList(); + } + } + } + + @After + public void cleanUp() { + testServerKey = null; + testUserKey = null; + } + + @Override + protected void installConfig(String... config) { + File configFile = new File(sshDir, Constants.CONFIG); + if (config != null) { + try { + Files.write(configFile.toPath(), Arrays.asList(config)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private KeyPair load(Path path) throws Exception { + try (InputStream in = Files.newInputStream(path)) { + return SecurityUtils + .loadKeyPairIdentities(null, + NamedResource.ofName(path.toString()), in, null) + .iterator().next(); + } + } + + @Test + public void testCloneWithBuiltInKeys() throws Exception { + // This test should fail unless our in-memory setup is taken: no + // known_hosts file, and a config that specifies a non-existing key. + File newHostKey = new File(getTemporaryDirectory(), "newhostkey"); + copyTestResource("id_ed25519", newHostKey); + server.addHostKey(newHostKey.toPath(), true); + testServerKey = load(newHostKey.toPath()).getPublic(); + assertTrue(newHostKey.delete()); + testUserKey = load(privateKey1.getAbsoluteFile().toPath()); + assertNotNull(testServerKey); + assertNotNull(testUserKey); + cloneWith( + "ssh://" + TEST_USER + "@localhost:" + testPort + + "/doesntmatter", + new File(getTemporaryDirectory(), "cloned"), null, // + "Host localhost", // + "IdentityFile " + + new File(sshDir, "does_not_exist").getAbsolutePath()); + } + +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java new file mode 100644 index 000000000..c1d10bd44 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitServerKeyVerifier.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2019 Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.sshd; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.PublicKey; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.config.hosts.KnownHostHashValue; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.SshConstants; +import org.eclipse.jgit.transport.sshd.ServerKeyDatabase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A bridge between the {@link ServerKeyVerifier} from Apache MINA sshd and our + * {@link ServerKeyDatabase}. + */ +public class JGitServerKeyVerifier + implements ServerKeyVerifier, ServerKeyLookup { + + private static final Logger LOG = LoggerFactory + .getLogger(JGitServerKeyVerifier.class); + + private final @NonNull ServerKeyDatabase database; + + /** + * Creates a new {@link JGitServerKeyVerifier} using the given + * {@link ServerKeyDatabase}. + * + * @param database + * to use + */ + public JGitServerKeyVerifier(@NonNull ServerKeyDatabase database) { + this.database = database; + } + + @Override + public List lookup(ClientSession session, + SocketAddress remoteAddress) { + if (!(session instanceof JGitClientSession)) { + LOG.warn("Internal error: wrong session kind: " //$NON-NLS-1$ + + session.getClass().getName()); + return Collections.emptyList(); + } + if (!(remoteAddress instanceof InetSocketAddress)) { + return Collections.emptyList(); + } + SessionConfig config = new SessionConfig((JGitClientSession) session); + SshdSocketAddress connectAddress = SshdSocketAddress + .toSshdSocketAddress(session.getConnectAddress()); + String connect = KnownHostHashValue.createHostPattern( + connectAddress.getHostName(), connectAddress.getPort()); + return database.lookup(connect, (InetSocketAddress) remoteAddress, + config); + } + + @Override + public boolean verifyServerKey(ClientSession session, + SocketAddress remoteAddress, PublicKey serverKey) { + if (!(session instanceof JGitClientSession)) { + LOG.warn("Internal error: wrong session kind: " //$NON-NLS-1$ + + session.getClass().getName()); + return false; + } + if (!(remoteAddress instanceof InetSocketAddress)) { + return false; + } + SessionConfig config = new SessionConfig((JGitClientSession) session); + SshdSocketAddress connectAddress = SshdSocketAddress + .toSshdSocketAddress(session.getConnectAddress()); + String connect = KnownHostHashValue.createHostPattern( + connectAddress.getHostName(), connectAddress.getPort()); + CredentialsProvider provider = ((JGitClientSession) session) + .getCredentialsProvider(); + return database.accept(connect, (InetSocketAddress) remoteAddress, + serverKey, config, provider); + } + + private static class SessionConfig + implements ServerKeyDatabase.Configuration { + + private final JGitClientSession session; + + public SessionConfig(JGitClientSession session) { + this.session = session; + } + + private List get(String key) { + HostConfigEntry entry = session.getHostConfigEntry(); + if (entry instanceof JGitHostConfigEntry) { + // Always true! + return ((JGitHostConfigEntry) entry).getMultiValuedOptions() + .get(key); + } + return Collections.emptyList(); + } + + @Override + public List getUserKnownHostsFiles() { + return get(SshConstants.USER_KNOWN_HOSTS_FILE); + } + + @Override + public List getGlobalKnownHostsFiles() { + return get(SshConstants.GLOBAL_KNOWN_HOSTS_FILE); + } + + @Override + public StrictHostKeyChecking getStrictHostKeyChecking() { + HostConfigEntry entry = session.getHostConfigEntry(); + String value = entry + .getProperty(SshConstants.STRICT_HOST_KEY_CHECKING, "ask"); //$NON-NLS-1$ + switch (value.toLowerCase(Locale.ROOT)) { + case SshConstants.YES: + case SshConstants.ON: + return StrictHostKeyChecking.REQUIRE_MATCH; + case SshConstants.NO: + case SshConstants.OFF: + return StrictHostKeyChecking.ACCEPT_ANY; + case "accept-new": //$NON-NLS-1$ + return StrictHostKeyChecking.ACCEPT_NEW; + default: + return StrictHostKeyChecking.ASK; + } + } + + @Override + public String getUsername() { + return session.getUsername(); + } + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java similarity index 86% rename from org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java rename to org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java index 40da9559c..e43310968 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Thomas Wolf + * Copyright (C) 2018, 2019 Thomas Wolf * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -64,17 +64,15 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; -import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.config.hosts.HostPatternsHolder; 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.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.apache.sshd.common.config.keys.KeyUtils; @@ -83,11 +81,12 @@ import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; import org.apache.sshd.common.digest.BuiltinDigests; import org.apache.sshd.common.util.io.ModifiableFileWatcher; import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.internal.storage.file.LockFile; import org.eclipse.jgit.transport.CredentialItem; import org.eclipse.jgit.transport.CredentialsProvider; -import org.eclipse.jgit.transport.SshConstants; import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.sshd.ServerKeyDatabase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -149,14 +148,14 @@ import org.slf4j.LoggerFactory; * @see man * ssh-config */ -public class OpenSshServerKeyVerifier - implements ServerKeyVerifier, ServerKeyLookup { +public class OpenSshServerKeyDatabase + implements ServerKeyDatabase { // TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these // files may be large! private static final Logger LOG = LoggerFactory - .getLogger(OpenSshServerKeyVerifier.class); + .getLogger(OpenSshServerKeyDatabase.class); /** Can be used to mark revoked known host lines. */ private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$ @@ -168,7 +167,7 @@ public class OpenSshServerKeyVerifier private final List defaultFiles = new ArrayList<>(); /** - * Creates a new {@link OpenSshServerKeyVerifier}. + * Creates a new {@link OpenSshServerKeyDatabase}. * * @param askAboutNewFile * whether to ask the user, if possible, about creating a new @@ -178,7 +177,7 @@ public class OpenSshServerKeyVerifier * empty or {@code null}, in which case no default files are * installed. The files need not exist. */ - public OpenSshServerKeyVerifier(boolean askAboutNewFile, + public OpenSshServerKeyDatabase(boolean askAboutNewFile, List defaultFiles) { if (defaultFiles != null) { for (Path file : defaultFiles) { @@ -190,31 +189,24 @@ public class OpenSshServerKeyVerifier this.askAboutNewFile = askAboutNewFile; } - private List getFilesToUse(ClientSession session) { + private List getFilesToUse(@NonNull Configuration config) { List filesToUse = defaultFiles; - if (session instanceof JGitClientSession) { - HostConfigEntry entry = ((JGitClientSession) session) - .getHostConfigEntry(); - if (entry instanceof JGitHostConfigEntry) { - // Always true! - List userFiles = addUserHostKeyFiles( - ((JGitHostConfigEntry) entry).getMultiValuedOptions() - .get(SshConstants.USER_KNOWN_HOSTS_FILE)); - if (!userFiles.isEmpty()) { - filesToUse = userFiles; - } - } + List userFiles = addUserHostKeyFiles( + config.getUserKnownHostsFiles()); + if (!userFiles.isEmpty()) { + filesToUse = userFiles; } return filesToUse; } @Override - public List lookup(ClientSession session, - SocketAddress remote) { - List filesToUse = getFilesToUse(session); + public List lookup(@NonNull String connectAddress, + @NonNull InetSocketAddress remoteAddress, + @NonNull Configuration config) { + List filesToUse = getFilesToUse(config); List result = new ArrayList<>(); Collection candidates = getCandidates( - session.getConnectAddress(), remote); + connectAddress, remoteAddress); for (HostKeyFile file : filesToUse) { for (HostEntryPair current : file.get()) { KnownHostEntry entry = current.getHostEntry(); @@ -230,14 +222,16 @@ public class OpenSshServerKeyVerifier } @Override - public boolean verifyServerKey(ClientSession clientSession, - SocketAddress remoteAddress, PublicKey serverKey) { - List filesToUse = getFilesToUse(clientSession); - AskUser ask = new AskUser(clientSession); + public boolean accept(@NonNull String connectAddress, + @NonNull InetSocketAddress remoteAddress, + @NonNull PublicKey serverKey, + @NonNull Configuration config, CredentialsProvider provider) { + List filesToUse = getFilesToUse(config); + AskUser ask = new AskUser(config, provider); HostEntryPair[] modified = { null }; Path path = null; - Collection candidates = getCandidates( - clientSession.getConnectAddress(), remoteAddress); + Collection candidates = getCandidates(connectAddress, + remoteAddress); for (HostKeyFile file : filesToUse) { try { if (find(candidates, serverKey, file.get(), modified)) { @@ -433,16 +427,14 @@ public class OpenSshServerKeyVerifier ASK, DENY, ALLOW; } - private final JGitClientSession session; + private final @NonNull Configuration config; - public AskUser(ClientSession clientSession) { - session = (clientSession instanceof JGitClientSession) - ? (JGitClientSession) clientSession - : null; - } + private final CredentialsProvider provider; - private CredentialsProvider getCredentialsProvider() { - return session == null ? null : session.getCredentialsProvider(); + public AskUser(@NonNull Configuration config, + CredentialsProvider provider) { + this.config = config; + this.provider = provider; } private static boolean askUser(CredentialsProvider provider, URIish uri, @@ -465,38 +457,25 @@ public class OpenSshServerKeyVerifier if (!(remoteAddress instanceof InetSocketAddress)) { return Check.DENY; } - HostConfigEntry entry = session.getHostConfigEntry(); - String value = entry - .getProperty(SshConstants.STRICT_HOST_KEY_CHECKING, "ask"); //$NON-NLS-1$ - switch (value.toLowerCase(Locale.ROOT)) { - case SshConstants.YES: - case SshConstants.ON: + switch (config.getStrictHostKeyChecking()) { + case REQUIRE_MATCH: return Check.DENY; - case SshConstants.NO: - case SshConstants.OFF: + case ACCEPT_ANY: return Check.ALLOW; - case "accept-new": //$NON-NLS-1$ + case ACCEPT_NEW: return changed ? Check.DENY : Check.ALLOW; default: - break; - } - if (getCredentialsProvider() == null) { - // This is called only for new, unknown hosts. If we have no way - // to interact with the user, the fallback mode is to deny the - // key. - return Check.DENY; + return provider == null ? Check.DENY : Check.ASK; } - return Check.ASK; } public void revokedKey(SocketAddress remoteAddress, PublicKey serverKey, Path path) { - CredentialsProvider provider = getCredentialsProvider(); if (provider == null) { return; } InetSocketAddress remote = (InetSocketAddress) remoteAddress; - URIish uri = JGitUserInteraction.toURI(session.getUsername(), + URIish uri = JGitUserInteraction.toURI(config.getUsername(), remote); String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256, serverKey); @@ -516,7 +495,6 @@ public class OpenSshServerKeyVerifier if (check != Check.ASK) { return check == Check.ALLOW; } - CredentialsProvider provider = getCredentialsProvider(); InetSocketAddress remote = (InetSocketAddress) remoteAddress; // Ask the user String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256, @@ -524,7 +502,7 @@ public class OpenSshServerKeyVerifier String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey); String keyAlgorithm = serverKey.getAlgorithm(); String remoteHost = remote.getHostString(); - URIish uri = JGitUserInteraction.toURI(session.getUsername(), + URIish uri = JGitUserInteraction.toURI(config.getUsername(), remote); String prompt = SshdText.get().knownHostsUnknownKeyPrompt; return askUser(provider, uri, prompt, // @@ -536,18 +514,17 @@ public class OpenSshServerKeyVerifier } public ModifiedKeyHandling acceptModifiedServerKey( - SocketAddress remoteAddress, PublicKey expected, + InetSocketAddress remoteAddress, PublicKey expected, PublicKey actual, Path path) { Check check = checkMode(remoteAddress, true); if (check == Check.ALLOW) { // Never auto-store on CHECK.ALLOW return ModifiedKeyHandling.ALLOW; } - InetSocketAddress remote = (InetSocketAddress) remoteAddress; String keyAlgorithm = actual.getAlgorithm(); - String remoteHost = remote.getHostString(); - URIish uri = JGitUserInteraction.toURI(session.getUsername(), - remote); + String remoteHost = remoteAddress.getHostString(); + URIish uri = JGitUserInteraction.toURI(config.getUsername(), + remoteAddress); List messages = new ArrayList<>(); String warning = format( SshdText.get().knownHostsModifiedKeyWarning, @@ -558,7 +535,6 @@ public class OpenSshServerKeyVerifier KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual)); messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$ - CredentialsProvider provider = getCredentialsProvider(); if (check == Check.DENY) { if (provider != null) { messages.add(format( @@ -587,7 +563,6 @@ public class OpenSshServerKeyVerifier } public boolean createNewFile(Path path) { - CredentialsProvider provider = getCredentialsProvider(); if (provider == null) { // We can't ask, so don't create the file return false; @@ -674,12 +649,56 @@ public class OpenSshServerKeyVerifier } } + private int parsePort(String s) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return -1; + } + } + + private SshdSocketAddress toSshdSocketAddress(@NonNull String address) { + String host = null; + int port = 0; + if (HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == address + .charAt(0)) { + int end = address.indexOf( + HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM); + if (end <= 1) { + return null; // Invalid + } + host = address.substring(1, end); + if (end < address.length() - 1 + && HostPatternsHolder.PORT_VALUE_DELIMITER == address + .charAt(end + 1)) { + port = parsePort(address.substring(end + 2)); + } + } else { + int i = address + .lastIndexOf(HostPatternsHolder.PORT_VALUE_DELIMITER); + if (i > 0) { + port = parsePort(address.substring(i + 1)); + host = address.substring(0, i); + } else { + host = address; + } + } + if (port < 0 || port > 65535) { + return null; + } + return new SshdSocketAddress(host, port); + } + private Collection getCandidates( - SocketAddress connectAddress, SocketAddress remoteAddress) { + @NonNull String connectAddress, + @NonNull InetSocketAddress remoteAddress) { Collection candidates = new TreeSet<>( SshdSocketAddress.BY_HOST_AND_PORT); candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress)); - candidates.add(SshdSocketAddress.toSshdSocketAddress(connectAddress)); + SshdSocketAddress address = toSshdSocketAddress(connectAddress); + if (address != null) { + candidates.add(address); + } return candidates; } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java new file mode 100644 index 000000000..ce58d9518 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ServerKeyDatabase.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2019 Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport.sshd; + +import java.net.InetSocketAddress; +import java.security.PublicKey; +import java.util.List; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.transport.CredentialsProvider; + +/** + * An interface for a database of known server keys, supporting finding all + * known keys and also deciding whether a server key is to be accepted. + *

+ * Connection addresses are given as strings of the format + * {@code [hostName]:port} if using a non-standard port (i.e., not port 22), + * otherwise just {@code hostname}. + *

+ * + * @since 5.5 + */ +public interface ServerKeyDatabase { + + /** + * Retrieves all known host keys for the given addresses. + * + * @param connectAddress + * IP address the session tried to connect to + * @param remoteAddress + * IP address as reported for the remote end point + * @param config + * giving access to potentially interesting configuration + * settings + * @return the list of known keys for the given addresses + */ + @NonNull + List lookup(@NonNull String connectAddress, + @NonNull InetSocketAddress remoteAddress, + @NonNull Configuration config); + + /** + * Determines whether to accept a received server host key. + * + * @param connectAddress + * IP address the session tried to connect to + * @param remoteAddress + * IP address as reported for the remote end point + * @param serverKey + * received from the remote end + * @param config + * giving access to potentially interesting configuration + * settings + * @param provider + * for interacting with the user, if required; may be + * {@code null} + * @return {@code true} if the serverKey is accepted, {@code false} + * otherwise + */ + boolean accept(@NonNull String connectAddress, + @NonNull InetSocketAddress remoteAddress, + @NonNull PublicKey serverKey, + @NonNull Configuration config, CredentialsProvider provider); + + /** + * A simple provider for ssh config settings related to host key checking. + * An instance is created by the JGit sshd framework and passed into + * {@link ServerKeyDatabase#lookup(String, InetSocketAddress, Configuration)} + * and + * {@link ServerKeyDatabase#accept(String, InetSocketAddress, PublicKey, Configuration, CredentialsProvider)}. + */ + interface Configuration { + + /** + * Retrieves the list of file names from the "UserKnownHostsFile" ssh + * config. + * + * @return the list as configured, with ~ already replaced + */ + List getUserKnownHostsFiles(); + + /** + * Retrieves the list of file names from the "GlobalKnownHostsFile" ssh + * config. + * + * @return the list as configured, with ~ already replaced + */ + List getGlobalKnownHostsFiles(); + + /** + * The possible values for the "StrictHostKeyChecking" ssh config. + */ + enum StrictHostKeyChecking { + /** + * "ask"; default: ask the user whether to accept (and store) a new + * or mismatched key. + */ + ASK, + /** + * "yes", "on": never accept new or mismatched keys. + */ + REQUIRE_MATCH, + /** + * "no", "off": always accept new or mismatched keys. + */ + ACCEPT_ANY, + /** + * "accept-new": accept new keys, but never accept modified keys. + */ + ACCEPT_NEW + } + + /** + * Obtains the value of the "StrictHostKeyChecking" ssh config. + * + * @return the {@link StrictHostKeyChecking} + */ + @NonNull + StrictHostKeyChecking getStrictHostKeyChecking(); + + /** + * Obtains the user name used in the connection attempt. + * + * @return the user name + */ + @NonNull + String getUsername(); + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java index ea5d81155..3460185d8 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Thomas Wolf + * Copyright (C) 2018, 2019 Thomas Wolf * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -66,7 +66,6 @@ import org.apache.sshd.client.auth.UserAuth; import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory; import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory; import org.apache.sshd.client.config.hosts.HostConfigEntryResolver; -import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.compression.BuiltinCompressions; import org.apache.sshd.common.config.keys.FilePasswordProvider; @@ -77,10 +76,11 @@ import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider; import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory; import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory; +import org.eclipse.jgit.internal.transport.sshd.JGitServerKeyVerifier; import org.eclipse.jgit.internal.transport.sshd.JGitSshClient; import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig; import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction; -import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyVerifier; +import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyDatabase; import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper; import org.eclipse.jgit.internal.transport.sshd.SshdText; import org.eclipse.jgit.transport.CredentialsProvider; @@ -104,7 +104,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { private final Map defaultHostConfigEntryResolver = new ConcurrentHashMap<>(); - private final Map defaultServerKeyVerifier = new ConcurrentHashMap<>(); + private final Map defaultServerKeyDatabase = new ConcurrentHashMap<>(); private final Map> defaultKeys = new ConcurrentHashMap<>(); @@ -226,7 +226,8 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { .filePasswordProvider( createFilePasswordProvider(passphrases)) .hostConfigEntryResolver(configFile) - .serverKeyVerifier(getServerKeyVerifier(home, sshDir)) + .serverKeyVerifier(new JGitServerKeyVerifier( + getServerKeyDatabase(home, sshDir))) .compressionFactories( new ArrayList<>(BuiltinCompressions.VALUES)) .build(); @@ -380,28 +381,28 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { } /** - * Obtain a {@link ServerKeyVerifier} to read known_hosts files and to - * verify server host keys. The default implementation returns a - * {@link ServerKeyVerifier} that recognizes the two openssh standard files - * {@code ~/.ssh/known_hosts} and {@code ~/.ssh/known_hosts2} as well as any - * files configured via the {@code UserKnownHostsFile} option in the ssh - * config file. + * Obtain a {@link ServerKeyDatabase} to verify server host keys. The + * default implementation returns a {@link ServerKeyDatabase} that + * recognizes the two openssh standard files {@code ~/.ssh/known_hosts} and + * {@code ~/.ssh/known_hosts2} as well as any files configured via the + * {@code UserKnownHostsFile} option in the ssh config file. * * @param homeDir * home directory to use for ~ replacement * @param sshDir * representing ~/.ssh/ - * @return the resolver + * @return the {@link ServerKeyDatabase} + * @since 5.5 */ @NonNull - private ServerKeyVerifier getServerKeyVerifier(@NonNull File homeDir, + protected ServerKeyDatabase getServerKeyDatabase(@NonNull File homeDir, @NonNull File sshDir) { - return defaultServerKeyVerifier.computeIfAbsent( + return defaultServerKeyDatabase.computeIfAbsent( new Tuple(new Object[] { homeDir, sshDir }), - t -> new OpenSshServerKeyVerifier(true, + t -> new OpenSshServerKeyDatabase(true, getDefaultKnownHostsFiles(sshDir))); - } + } /** * Gets the list of default user known hosts files. The default returns * ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config @@ -554,7 +555,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { * the ssh config defines {@code PreferredAuthentications} the value from * the ssh config takes precedence. * - * @return a comma-separated list of algorithm names, or {@code null} if + * @return a comma-separated list of mechanism names, or {@code null} if * none */ protected String getDefaultPreferredAuthentications() {