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)",
--- /dev/null
+/*
+ * Copyright (C) 2019 Thomas Wolf <thomas.wolf@paranor.ch>
+ * 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<PublicKey> 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<KeyPair> 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<KeyPair> {
+
+ @Override
+ public Iterator<KeyPair> 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<KeyPair> 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());
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2019 Thomas Wolf <thomas.wolf@paranor.ch>
+ * 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<PublicKey> 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<String> 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<String> getUserKnownHostsFiles() {
+ return get(SshConstants.USER_KNOWN_HOSTS_FILE);
+ }
+
+ @Override
+ public List<String> 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();
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018, 2019 Thomas Wolf <thomas.wolf@paranor.ch>
+ * 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 static java.nio.charset.StandardCharsets.UTF_8;
+import static java.text.MessageFormat.format;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+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.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.session.ClientSession;
+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.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.URIish;
+import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A sever host key verifier that honors the {@code StrictHostKeyChecking} and
+ * {@code UserKnownHostsFile} values from the ssh configuration.
+ * <p>
+ * The verifier can be given default known_hosts files in the constructor, which
+ * will be used if the ssh config does not specify a {@code UserKnownHostsFile}.
+ * If the ssh config <em>does</em> set {@code UserKnownHostsFile}, the verifier
+ * uses the given files in the order given. Non-existing or unreadable files are
+ * ignored.
+ * <p>
+ * {@code StrictHostKeyChecking} accepts the following values:
+ * </p>
+ * <dl>
+ * <dt>ask</dt>
+ * <dd>Ask the user whether new or changed keys shall be accepted and be added
+ * to the known_hosts file.</dd>
+ * <dt>yes/true</dt>
+ * <dd>Accept only keys listed in the known_hosts file.</dd>
+ * <dt>no/false</dt>
+ * <dd>Silently accept all new or changed keys, add new keys to the known_hosts
+ * file.</dd>
+ * <dt>accept-new</dt>
+ * <dd>Silently accept keys for new hosts and add them to the known_hosts
+ * file.</dd>
+ * </dl>
+ * <p>
+ * If {@code StrictHostKeyChecking} is not set, or set to any other value, the
+ * default value <b>ask</b> is active.
+ * </p>
+ * <p>
+ * This implementation relies on the {@link ClientSession} being a
+ * {@link JGitClientSession}. By default Apache MINA sshd does not forward the
+ * config file host entry to the session, so it would be unknown here which
+ * entry it was and what setting of {@code StrictHostKeyChecking} should be
+ * used. If used with some other session type, the implementation assumes
+ * "<b>ask</b>".
+ * <p>
+ * <p>
+ * Asking the user is done via a {@link CredentialsProvider} obtained from the
+ * session. If none is set, the implementation falls back to strict host key
+ * checking ("<b>yes</b>").
+ * </p>
+ * <p>
+ * Note that adding a key to the known hosts file may create the file. You can
+ * specify in the constructor whether the user shall be asked about that, too.
+ * If the user declines updating the file, but the key was otherwise
+ * accepted (user confirmed for "<b>ask</b>", or "no" or "accept-new" are
+ * active), the key is accepted for this session only.
+ * </p>
+ * <p>
+ * If several known hosts files are specified, a new key is always added to the
+ * first file (even if it doesn't exist yet; see the note about file creation
+ * above).
+ * </p>
+ *
+ * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
+ * ssh-config</a>
+ */
+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(OpenSshServerKeyDatabase.class);
+
+ /** Can be used to mark revoked known host lines. */
+ private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$
+
+ private final boolean askAboutNewFile;
+
+ private final Map<Path, HostKeyFile> knownHostsFiles = new ConcurrentHashMap<>();
+
+ private final List<HostKeyFile> defaultFiles = new ArrayList<>();
+
+ /**
+ * Creates a new {@link OpenSshServerKeyDatabase}.
+ *
+ * @param askAboutNewFile
+ * whether to ask the user, if possible, about creating a new
+ * non-existing known_hosts file
+ * @param defaultFiles
+ * typically ~/.ssh/known_hosts and ~/.ssh/known_hosts2. May be
+ * empty or {@code null}, in which case no default files are
+ * installed. The files need not exist.
+ */
+ public OpenSshServerKeyDatabase(boolean askAboutNewFile,
+ List<Path> defaultFiles) {
+ if (defaultFiles != null) {
+ for (Path file : defaultFiles) {
+ HostKeyFile newFile = new HostKeyFile(file);
+ knownHostsFiles.put(file, newFile);
+ this.defaultFiles.add(newFile);
+ }
+ }
+ this.askAboutNewFile = askAboutNewFile;
+ }
+
+ private List<HostKeyFile> getFilesToUse(@NonNull Configuration config) {
+ List<HostKeyFile> filesToUse = defaultFiles;
+ List<HostKeyFile> userFiles = addUserHostKeyFiles(
+ config.getUserKnownHostsFiles());
+ if (!userFiles.isEmpty()) {
+ filesToUse = userFiles;
+ }
+ return filesToUse;
+ }
+
+ @Override
+ public List<PublicKey> lookup(@NonNull String connectAddress,
+ @NonNull InetSocketAddress remoteAddress,
+ @NonNull Configuration config) {
+ List<HostKeyFile> filesToUse = getFilesToUse(config);
+ List<PublicKey> result = new ArrayList<>();
+ Collection<SshdSocketAddress> candidates = getCandidates(
+ connectAddress, remoteAddress);
+ for (HostKeyFile file : filesToUse) {
+ for (HostEntryPair current : file.get()) {
+ KnownHostEntry entry = current.getHostEntry();
+ for (SshdSocketAddress host : candidates) {
+ if (entry.isHostMatch(host.getHostName(), host.getPort())) {
+ result.add(current.getServerKey());
+ break;
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean accept(@NonNull String connectAddress,
+ @NonNull InetSocketAddress remoteAddress,
+ @NonNull PublicKey serverKey,
+ @NonNull Configuration config, CredentialsProvider provider) {
+ List<HostKeyFile> filesToUse = getFilesToUse(config);
+ AskUser ask = new AskUser(config, provider);
+ HostEntryPair[] modified = { null };
+ Path path = null;
+ Collection<SshdSocketAddress> candidates = getCandidates(connectAddress,
+ remoteAddress);
+ for (HostKeyFile file : filesToUse) {
+ try {
+ if (find(candidates, serverKey, file.get(), modified)) {
+ return true;
+ }
+ } catch (RevokedKeyException e) {
+ ask.revokedKey(remoteAddress, serverKey, file.getPath());
+ return false;
+ }
+ if (path == null && modified[0] != null) {
+ // Remember the file in which we might need to update the
+ // entry
+ path = file.getPath();
+ }
+ }
+ if (modified[0] != null) {
+ // We found an entry, but with a different key
+ AskUser.ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
+ remoteAddress, modified[0].getServerKey(),
+ serverKey, path);
+ if (toDo == AskUser.ModifiedKeyHandling.ALLOW_AND_STORE) {
+ try {
+ updateModifiedServerKey(serverKey, modified[0], path);
+ knownHostsFiles.get(path).resetReloadAttributes();
+ } catch (IOException e) {
+ LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
+ path));
+ }
+ }
+ if (toDo == AskUser.ModifiedKeyHandling.DENY) {
+ return false;
+ }
+ // TODO: OpenSsh disables password and keyboard-interactive
+ // authentication in this case. Also agent and local port forwarding
+ // are switched off. (Plus a few other things such as X11 forwarding
+ // that are of no interest to a git client.)
+ return true;
+ } else if (ask.acceptUnknownKey(remoteAddress, serverKey)) {
+ if (!filesToUse.isEmpty()) {
+ HostKeyFile toUpdate = filesToUse.get(0);
+ path = toUpdate.getPath();
+ try {
+ if (Files.exists(path) || !askAboutNewFile
+ || ask.createNewFile(path)) {
+ updateKnownHostsFile(candidates, serverKey, path);
+ toUpdate.resetReloadAttributes();
+ }
+ } catch (IOException e) {
+ LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
+ path));
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private static class RevokedKeyException extends Exception {
+ private static final long serialVersionUID = 1L;
+ }
+
+ private boolean find(Collection<SshdSocketAddress> candidates,
+ PublicKey serverKey, List<HostEntryPair> entries,
+ HostEntryPair[] modified) throws RevokedKeyException {
+ for (HostEntryPair current : entries) {
+ KnownHostEntry entry = current.getHostEntry();
+ for (SshdSocketAddress host : candidates) {
+ if (entry.isHostMatch(host.getHostName(), host.getPort())) {
+ boolean isRevoked = MARKER_REVOKED
+ .equals(entry.getMarker());
+ if (KeyUtils.compareKeys(serverKey,
+ current.getServerKey())) {
+ // Exact match
+ if (isRevoked) {
+ throw new RevokedKeyException();
+ }
+ modified[0] = null;
+ return true;
+ } else if (!isRevoked) {
+ // Server sent a different key
+ modified[0] = current;
+ // Keep going -- maybe there's another entry for this
+ // host
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
+ if (fileNames == null || fileNames.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<HostKeyFile> userFiles = new ArrayList<>();
+ for (String name : fileNames) {
+ try {
+ Path path = Paths.get(name);
+ HostKeyFile file = knownHostsFiles.computeIfAbsent(path,
+ p -> new HostKeyFile(path));
+ userFiles.add(file);
+ } catch (InvalidPathException e) {
+ LOG.warn(format(SshdText.get().knownHostsInvalidPath,
+ name));
+ }
+ }
+ return userFiles;
+ }
+
+ private void updateKnownHostsFile(Collection<SshdSocketAddress> candidates,
+ PublicKey serverKey, Path path)
+ throws IOException {
+ String newEntry = createHostKeyLine(candidates, serverKey);
+ if (newEntry == null) {
+ return;
+ }
+ LockFile lock = new LockFile(path.toFile());
+ if (lock.lockForAppend()) {
+ try {
+ try (BufferedWriter writer = new BufferedWriter(
+ new OutputStreamWriter(lock.getOutputStream(),
+ UTF_8))) {
+ writer.newLine();
+ writer.write(newEntry);
+ writer.newLine();
+ }
+ lock.commit();
+ } catch (IOException e) {
+ lock.unlock();
+ throw e;
+ }
+ } else {
+ LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
+ path));
+ }
+ }
+
+ private void updateModifiedServerKey(PublicKey serverKey,
+ HostEntryPair entry, Path path)
+ throws IOException {
+ KnownHostEntry hostEntry = entry.getHostEntry();
+ String oldLine = hostEntry.getConfigLine();
+ String newLine = updateHostKeyLine(oldLine, serverKey);
+ if (newLine == null || newLine.isEmpty()) {
+ return;
+ }
+ if (oldLine == null || oldLine.isEmpty() || newLine.equals(oldLine)) {
+ // Shouldn't happen.
+ return;
+ }
+ LockFile lock = new LockFile(path.toFile());
+ if (lock.lock()) {
+ try {
+ try (BufferedWriter writer = new BufferedWriter(
+ new OutputStreamWriter(lock.getOutputStream(), UTF_8));
+ BufferedReader reader = Files.newBufferedReader(path,
+ UTF_8)) {
+ boolean done = false;
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String toWrite = line;
+ if (!done) {
+ int pos = line.indexOf('#');
+ String toTest = pos < 0 ? line
+ : line.substring(0, pos);
+ if (toTest.trim().equals(oldLine)) {
+ toWrite = newLine;
+ done = true;
+ }
+ }
+ writer.write(toWrite);
+ writer.newLine();
+ }
+ }
+ lock.commit();
+ } catch (IOException e) {
+ lock.unlock();
+ throw e;
+ }
+ } else {
+ LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
+ path));
+ }
+ }
+
+ private static class AskUser {
+
+ public enum ModifiedKeyHandling {
+ DENY, ALLOW, ALLOW_AND_STORE
+ }
+
+ private enum Check {
+ ASK, DENY, ALLOW;
+ }
+
+ private final @NonNull Configuration config;
+
+ private final CredentialsProvider provider;
+
+ public AskUser(@NonNull Configuration config,
+ CredentialsProvider provider) {
+ this.config = config;
+ this.provider = provider;
+ }
+
+ private static boolean askUser(CredentialsProvider provider, URIish uri,
+ String prompt, String... messages) {
+ List<CredentialItem> items = new ArrayList<>(messages.length + 1);
+ for (String message : messages) {
+ items.add(new CredentialItem.InformationalMessage(message));
+ }
+ if (prompt != null) {
+ CredentialItem.YesNoType answer = new CredentialItem.YesNoType(
+ prompt);
+ items.add(answer);
+ return provider.get(uri, items) && answer.getValue();
+ } else {
+ return provider.get(uri, items);
+ }
+ }
+
+ private Check checkMode(SocketAddress remoteAddress, boolean changed) {
+ if (!(remoteAddress instanceof InetSocketAddress)) {
+ return Check.DENY;
+ }
+ switch (config.getStrictHostKeyChecking()) {
+ case REQUIRE_MATCH:
+ return Check.DENY;
+ case ACCEPT_ANY:
+ return Check.ALLOW;
+ case ACCEPT_NEW:
+ return changed ? Check.DENY : Check.ALLOW;
+ default:
+ return provider == null ? Check.DENY : Check.ASK;
+ }
+ }
+
+ public void revokedKey(SocketAddress remoteAddress, PublicKey serverKey,
+ Path path) {
+ if (provider == null) {
+ return;
+ }
+ InetSocketAddress remote = (InetSocketAddress) remoteAddress;
+ URIish uri = JGitUserInteraction.toURI(config.getUsername(),
+ remote);
+ String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
+ serverKey);
+ String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
+ String keyAlgorithm = serverKey.getAlgorithm();
+ askUser(provider, uri, null, //
+ format(SshdText.get().knownHostsRevokedKeyMsg,
+ remote.getHostString(), path),
+ format(SshdText.get().knownHostsKeyFingerprints,
+ keyAlgorithm),
+ md5, sha256);
+ }
+
+ public boolean acceptUnknownKey(SocketAddress remoteAddress,
+ PublicKey serverKey) {
+ Check check = checkMode(remoteAddress, false);
+ if (check != Check.ASK) {
+ return check == Check.ALLOW;
+ }
+ InetSocketAddress remote = (InetSocketAddress) remoteAddress;
+ // Ask the user
+ String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
+ serverKey);
+ String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
+ String keyAlgorithm = serverKey.getAlgorithm();
+ String remoteHost = remote.getHostString();
+ URIish uri = JGitUserInteraction.toURI(config.getUsername(),
+ remote);
+ String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
+ return askUser(provider, uri, prompt, //
+ format(SshdText.get().knownHostsUnknownKeyMsg,
+ remoteHost),
+ format(SshdText.get().knownHostsKeyFingerprints,
+ keyAlgorithm),
+ md5, sha256);
+ }
+
+ public ModifiedKeyHandling acceptModifiedServerKey(
+ 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;
+ }
+ String keyAlgorithm = actual.getAlgorithm();
+ String remoteHost = remoteAddress.getHostString();
+ URIish uri = JGitUserInteraction.toURI(config.getUsername(),
+ remoteAddress);
+ List<String> messages = new ArrayList<>();
+ String warning = format(
+ SshdText.get().knownHostsModifiedKeyWarning,
+ keyAlgorithm, expected.getAlgorithm(), remoteHost,
+ KeyUtils.getFingerPrint(BuiltinDigests.md5, expected),
+ KeyUtils.getFingerPrint(BuiltinDigests.sha256, expected),
+ KeyUtils.getFingerPrint(BuiltinDigests.md5, actual),
+ KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual));
+ messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$
+
+ if (check == Check.DENY) {
+ if (provider != null) {
+ messages.add(format(
+ SshdText.get().knownHostsModifiedKeyDenyMsg, path));
+ askUser(provider, uri, null,
+ messages.toArray(new String[0]));
+ }
+ return ModifiedKeyHandling.DENY;
+ }
+ // ASK -- two questions: procceed? and store?
+ List<CredentialItem> items = new ArrayList<>(messages.size() + 2);
+ for (String message : messages) {
+ items.add(new CredentialItem.InformationalMessage(message));
+ }
+ CredentialItem.YesNoType proceed = new CredentialItem.YesNoType(
+ SshdText.get().knownHostsModifiedKeyAcceptPrompt);
+ CredentialItem.YesNoType store = new CredentialItem.YesNoType(
+ SshdText.get().knownHostsModifiedKeyStorePrompt);
+ items.add(proceed);
+ items.add(store);
+ if (provider.get(uri, items) && proceed.getValue()) {
+ return store.getValue() ? ModifiedKeyHandling.ALLOW_AND_STORE
+ : ModifiedKeyHandling.ALLOW;
+ }
+ return ModifiedKeyHandling.DENY;
+ }
+
+ public boolean createNewFile(Path path) {
+ if (provider == null) {
+ // We can't ask, so don't create the file
+ return false;
+ }
+ URIish uri = new URIish().setPath(path.toString());
+ return askUser(provider, uri, //
+ format(SshdText.get().knownHostsUserAskCreationPrompt,
+ path), //
+ format(SshdText.get().knownHostsUserAskCreationMsg, path));
+ }
+ }
+
+ private static class HostKeyFile extends ModifiableFileWatcher
+ implements Supplier<List<HostEntryPair>> {
+
+ private List<HostEntryPair> entries = Collections.emptyList();
+
+ public HostKeyFile(Path path) {
+ super(path);
+ }
+
+ @Override
+ public List<HostEntryPair> get() {
+ Path path = getPath();
+ try {
+ if (checkReloadRequired()) {
+ if (!Files.exists(path)) {
+ // Has disappeared.
+ resetReloadAttributes();
+ return Collections.emptyList();
+ }
+ LockFile lock = new LockFile(path.toFile());
+ if (lock.lock()) {
+ try {
+ entries = reload(getPath());
+ } finally {
+ lock.unlock();
+ }
+ } else {
+ LOG.warn(format(SshdText.get().knownHostsFileLockedRead,
+ path));
+ }
+ }
+ } catch (IOException e) {
+ LOG.warn(format(SshdText.get().knownHostsFileReadFailed, path));
+ }
+ return Collections.unmodifiableList(entries);
+ }
+
+ private List<HostEntryPair> reload(Path path) throws IOException {
+ try {
+ List<KnownHostEntry> rawEntries = KnownHostEntryReader
+ .readFromFile(path);
+ updateReloadAttributes();
+ if (rawEntries == null || rawEntries.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<HostEntryPair> newEntries = new LinkedList<>();
+ for (KnownHostEntry entry : rawEntries) {
+ AuthorizedKeyEntry keyPart = entry.getKeyEntry();
+ if (keyPart == null) {
+ continue;
+ }
+ try {
+ PublicKey serverKey = keyPart.resolvePublicKey(null,
+ PublicKeyEntryResolver.IGNORING);
+ if (serverKey == null) {
+ LOG.warn(format(
+ SshdText.get().knownHostsUnknownKeyType,
+ path, entry.getConfigLine()));
+ } else {
+ newEntries.add(new HostEntryPair(entry, serverKey));
+ }
+ } catch (GeneralSecurityException e) {
+ LOG.warn(format(SshdText.get().knownHostsInvalidLine,
+ path, entry.getConfigLine()));
+ }
+ }
+ return newEntries;
+ } catch (FileNotFoundException e) {
+ resetReloadAttributes();
+ return Collections.emptyList();
+ }
+ }
+ }
+
+ 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<SshdSocketAddress> getCandidates(
+ @NonNull String connectAddress,
+ @NonNull InetSocketAddress remoteAddress) {
+ Collection<SshdSocketAddress> candidates = new TreeSet<>(
+ SshdSocketAddress.BY_HOST_AND_PORT);
+ candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress));
+ SshdSocketAddress address = toSshdSocketAddress(connectAddress);
+ if (address != null) {
+ candidates.add(address);
+ }
+ return candidates;
+ }
+
+ private String createHostKeyLine(Collection<SshdSocketAddress> patterns,
+ PublicKey key) throws IOException {
+ StringBuilder result = new StringBuilder();
+ for (SshdSocketAddress address : patterns) {
+ if (result.length() > 0) {
+ result.append(',');
+ }
+ KnownHostHashValue.appendHostPattern(result, address.getHostName(),
+ address.getPort());
+ }
+ result.append(' ');
+ PublicKeyEntry.appendPublicKeyEntry(result, key);
+ return result.toString();
+ }
+
+ private String updateHostKeyLine(String line, PublicKey newKey)
+ throws IOException {
+ // Replaces an existing public key by the new key
+ int pos = line.indexOf(' ');
+ if (pos > 0 && line.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
+ // We're at the end of the marker. Skip ahead to the next blank.
+ pos = line.indexOf(' ', pos + 1);
+ }
+ if (pos < 0) {
+ // Don't update if bogus format
+ return null;
+ }
+ StringBuilder result = new StringBuilder(line.substring(0, pos + 1));
+ PublicKeyEntry.appendPublicKeyEntry(result, newKey);
+ return result.toString();
+ }
+
+}
+++ /dev/null
-/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
- * 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 static java.nio.charset.StandardCharsets.UTF_8;
-import static java.text.MessageFormat.format;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.net.InetSocketAddress;
-import java.net.SocketAddress;
-import java.nio.file.Files;
-import java.nio.file.InvalidPathException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.security.GeneralSecurityException;
-import java.security.PublicKey;
-import java.util.ArrayList;
-import java.util.Arrays;
-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.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;
-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.util.io.ModifiableFileWatcher;
-import org.apache.sshd.common.util.net.SshdSocketAddress;
-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.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A sever host key verifier that honors the {@code StrictHostKeyChecking} and
- * {@code UserKnownHostsFile} values from the ssh configuration.
- * <p>
- * The verifier can be given default known_hosts files in the constructor, which
- * will be used if the ssh config does not specify a {@code UserKnownHostsFile}.
- * If the ssh config <em>does</em> set {@code UserKnownHostsFile}, the verifier
- * uses the given files in the order given. Non-existing or unreadable files are
- * ignored.
- * <p>
- * {@code StrictHostKeyChecking} accepts the following values:
- * </p>
- * <dl>
- * <dt>ask</dt>
- * <dd>Ask the user whether new or changed keys shall be accepted and be added
- * to the known_hosts file.</dd>
- * <dt>yes/true</dt>
- * <dd>Accept only keys listed in the known_hosts file.</dd>
- * <dt>no/false</dt>
- * <dd>Silently accept all new or changed keys, add new keys to the known_hosts
- * file.</dd>
- * <dt>accept-new</dt>
- * <dd>Silently accept keys for new hosts and add them to the known_hosts
- * file.</dd>
- * </dl>
- * <p>
- * If {@code StrictHostKeyChecking} is not set, or set to any other value, the
- * default value <b>ask</b> is active.
- * </p>
- * <p>
- * This implementation relies on the {@link ClientSession} being a
- * {@link JGitClientSession}. By default Apache MINA sshd does not forward the
- * config file host entry to the session, so it would be unknown here which
- * entry it was and what setting of {@code StrictHostKeyChecking} should be
- * used. If used with some other session type, the implementation assumes
- * "<b>ask</b>".
- * <p>
- * <p>
- * Asking the user is done via a {@link CredentialsProvider} obtained from the
- * session. If none is set, the implementation falls back to strict host key
- * checking ("<b>yes</b>").
- * </p>
- * <p>
- * Note that adding a key to the known hosts file may create the file. You can
- * specify in the constructor whether the user shall be asked about that, too.
- * If the user declines updating the file, but the key was otherwise
- * accepted (user confirmed for "<b>ask</b>", or "no" or "accept-new" are
- * active), the key is accepted for this session only.
- * </p>
- * <p>
- * If several known hosts files are specified, a new key is always added to the
- * first file (even if it doesn't exist yet; see the note about file creation
- * above).
- * </p>
- *
- * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
- * ssh-config</a>
- */
-public class OpenSshServerKeyVerifier
- implements ServerKeyVerifier, ServerKeyLookup {
-
- // TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these
- // files may be large!
-
- private static final Logger LOG = LoggerFactory
- .getLogger(OpenSshServerKeyVerifier.class);
-
- /** Can be used to mark revoked known host lines. */
- private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$
-
- private final boolean askAboutNewFile;
-
- private final Map<Path, HostKeyFile> knownHostsFiles = new ConcurrentHashMap<>();
-
- private final List<HostKeyFile> defaultFiles = new ArrayList<>();
-
- /**
- * Creates a new {@link OpenSshServerKeyVerifier}.
- *
- * @param askAboutNewFile
- * whether to ask the user, if possible, about creating a new
- * non-existing known_hosts file
- * @param defaultFiles
- * typically ~/.ssh/known_hosts and ~/.ssh/known_hosts2. May be
- * empty or {@code null}, in which case no default files are
- * installed. The files need not exist.
- */
- public OpenSshServerKeyVerifier(boolean askAboutNewFile,
- List<Path> defaultFiles) {
- if (defaultFiles != null) {
- for (Path file : defaultFiles) {
- HostKeyFile newFile = new HostKeyFile(file);
- knownHostsFiles.put(file, newFile);
- this.defaultFiles.add(newFile);
- }
- }
- this.askAboutNewFile = askAboutNewFile;
- }
-
- private List<HostKeyFile> getFilesToUse(ClientSession session) {
- List<HostKeyFile> filesToUse = defaultFiles;
- if (session instanceof JGitClientSession) {
- HostConfigEntry entry = ((JGitClientSession) session)
- .getHostConfigEntry();
- if (entry instanceof JGitHostConfigEntry) {
- // Always true!
- List<HostKeyFile> userFiles = addUserHostKeyFiles(
- ((JGitHostConfigEntry) entry).getMultiValuedOptions()
- .get(SshConstants.USER_KNOWN_HOSTS_FILE));
- if (!userFiles.isEmpty()) {
- filesToUse = userFiles;
- }
- }
- }
- return filesToUse;
- }
-
- @Override
- public List<PublicKey> lookup(ClientSession session,
- SocketAddress remote) {
- List<HostKeyFile> filesToUse = getFilesToUse(session);
- List<PublicKey> result = new ArrayList<>();
- Collection<SshdSocketAddress> candidates = getCandidates(
- session.getConnectAddress(), remote);
- for (HostKeyFile file : filesToUse) {
- for (HostEntryPair current : file.get()) {
- KnownHostEntry entry = current.getHostEntry();
- for (SshdSocketAddress host : candidates) {
- if (entry.isHostMatch(host.getHostName(), host.getPort())) {
- result.add(current.getServerKey());
- break;
- }
- }
- }
- }
- return result;
- }
-
- @Override
- public boolean verifyServerKey(ClientSession clientSession,
- SocketAddress remoteAddress, PublicKey serverKey) {
- List<HostKeyFile> filesToUse = getFilesToUse(clientSession);
- AskUser ask = new AskUser(clientSession);
- HostEntryPair[] modified = { null };
- Path path = null;
- Collection<SshdSocketAddress> candidates = getCandidates(
- clientSession.getConnectAddress(), remoteAddress);
- for (HostKeyFile file : filesToUse) {
- try {
- if (find(candidates, serverKey, file.get(), modified)) {
- return true;
- }
- } catch (RevokedKeyException e) {
- ask.revokedKey(remoteAddress, serverKey, file.getPath());
- return false;
- }
- if (path == null && modified[0] != null) {
- // Remember the file in which we might need to update the
- // entry
- path = file.getPath();
- }
- }
- if (modified[0] != null) {
- // We found an entry, but with a different key
- AskUser.ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
- remoteAddress, modified[0].getServerKey(),
- serverKey, path);
- if (toDo == AskUser.ModifiedKeyHandling.ALLOW_AND_STORE) {
- try {
- updateModifiedServerKey(serverKey, modified[0], path);
- knownHostsFiles.get(path).resetReloadAttributes();
- } catch (IOException e) {
- LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
- path));
- }
- }
- if (toDo == AskUser.ModifiedKeyHandling.DENY) {
- return false;
- }
- // TODO: OpenSsh disables password and keyboard-interactive
- // authentication in this case. Also agent and local port forwarding
- // are switched off. (Plus a few other things such as X11 forwarding
- // that are of no interest to a git client.)
- return true;
- } else if (ask.acceptUnknownKey(remoteAddress, serverKey)) {
- if (!filesToUse.isEmpty()) {
- HostKeyFile toUpdate = filesToUse.get(0);
- path = toUpdate.getPath();
- try {
- if (Files.exists(path) || !askAboutNewFile
- || ask.createNewFile(path)) {
- updateKnownHostsFile(candidates, serverKey, path);
- toUpdate.resetReloadAttributes();
- }
- } catch (IOException e) {
- LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
- path));
- }
- }
- return true;
- }
- return false;
- }
-
- private static class RevokedKeyException extends Exception {
- private static final long serialVersionUID = 1L;
- }
-
- private boolean find(Collection<SshdSocketAddress> candidates,
- PublicKey serverKey, List<HostEntryPair> entries,
- HostEntryPair[] modified) throws RevokedKeyException {
- for (HostEntryPair current : entries) {
- KnownHostEntry entry = current.getHostEntry();
- for (SshdSocketAddress host : candidates) {
- if (entry.isHostMatch(host.getHostName(), host.getPort())) {
- boolean isRevoked = MARKER_REVOKED
- .equals(entry.getMarker());
- if (KeyUtils.compareKeys(serverKey,
- current.getServerKey())) {
- // Exact match
- if (isRevoked) {
- throw new RevokedKeyException();
- }
- modified[0] = null;
- return true;
- } else if (!isRevoked) {
- // Server sent a different key
- modified[0] = current;
- // Keep going -- maybe there's another entry for this
- // host
- }
- }
- }
- }
- return false;
- }
-
- private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
- if (fileNames == null || fileNames.isEmpty()) {
- return Collections.emptyList();
- }
- List<HostKeyFile> userFiles = new ArrayList<>();
- for (String name : fileNames) {
- try {
- Path path = Paths.get(name);
- HostKeyFile file = knownHostsFiles.computeIfAbsent(path,
- p -> new HostKeyFile(path));
- userFiles.add(file);
- } catch (InvalidPathException e) {
- LOG.warn(format(SshdText.get().knownHostsInvalidPath,
- name));
- }
- }
- return userFiles;
- }
-
- private void updateKnownHostsFile(Collection<SshdSocketAddress> candidates,
- PublicKey serverKey, Path path)
- throws IOException {
- String newEntry = createHostKeyLine(candidates, serverKey);
- if (newEntry == null) {
- return;
- }
- LockFile lock = new LockFile(path.toFile());
- if (lock.lockForAppend()) {
- try {
- try (BufferedWriter writer = new BufferedWriter(
- new OutputStreamWriter(lock.getOutputStream(),
- UTF_8))) {
- writer.newLine();
- writer.write(newEntry);
- writer.newLine();
- }
- lock.commit();
- } catch (IOException e) {
- lock.unlock();
- throw e;
- }
- } else {
- LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
- path));
- }
- }
-
- private void updateModifiedServerKey(PublicKey serverKey,
- HostEntryPair entry, Path path)
- throws IOException {
- KnownHostEntry hostEntry = entry.getHostEntry();
- String oldLine = hostEntry.getConfigLine();
- String newLine = updateHostKeyLine(oldLine, serverKey);
- if (newLine == null || newLine.isEmpty()) {
- return;
- }
- if (oldLine == null || oldLine.isEmpty() || newLine.equals(oldLine)) {
- // Shouldn't happen.
- return;
- }
- LockFile lock = new LockFile(path.toFile());
- if (lock.lock()) {
- try {
- try (BufferedWriter writer = new BufferedWriter(
- new OutputStreamWriter(lock.getOutputStream(), UTF_8));
- BufferedReader reader = Files.newBufferedReader(path,
- UTF_8)) {
- boolean done = false;
- String line;
- while ((line = reader.readLine()) != null) {
- String toWrite = line;
- if (!done) {
- int pos = line.indexOf('#');
- String toTest = pos < 0 ? line
- : line.substring(0, pos);
- if (toTest.trim().equals(oldLine)) {
- toWrite = newLine;
- done = true;
- }
- }
- writer.write(toWrite);
- writer.newLine();
- }
- }
- lock.commit();
- } catch (IOException e) {
- lock.unlock();
- throw e;
- }
- } else {
- LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
- path));
- }
- }
-
- private static class AskUser {
-
- public enum ModifiedKeyHandling {
- DENY, ALLOW, ALLOW_AND_STORE
- }
-
- private enum Check {
- ASK, DENY, ALLOW;
- }
-
- private final JGitClientSession session;
-
- public AskUser(ClientSession clientSession) {
- session = (clientSession instanceof JGitClientSession)
- ? (JGitClientSession) clientSession
- : null;
- }
-
- private CredentialsProvider getCredentialsProvider() {
- return session == null ? null : session.getCredentialsProvider();
- }
-
- private static boolean askUser(CredentialsProvider provider, URIish uri,
- String prompt, String... messages) {
- List<CredentialItem> items = new ArrayList<>(messages.length + 1);
- for (String message : messages) {
- items.add(new CredentialItem.InformationalMessage(message));
- }
- if (prompt != null) {
- CredentialItem.YesNoType answer = new CredentialItem.YesNoType(
- prompt);
- items.add(answer);
- return provider.get(uri, items) && answer.getValue();
- } else {
- return provider.get(uri, items);
- }
- }
-
- private Check checkMode(SocketAddress remoteAddress, boolean changed) {
- 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:
- return Check.DENY;
- case SshConstants.NO:
- case SshConstants.OFF:
- return Check.ALLOW;
- case "accept-new": //$NON-NLS-1$
- 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 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(),
- remote);
- String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
- serverKey);
- String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
- String keyAlgorithm = serverKey.getAlgorithm();
- askUser(provider, uri, null, //
- format(SshdText.get().knownHostsRevokedKeyMsg,
- remote.getHostString(), path),
- format(SshdText.get().knownHostsKeyFingerprints,
- keyAlgorithm),
- md5, sha256);
- }
-
- public boolean acceptUnknownKey(SocketAddress remoteAddress,
- PublicKey serverKey) {
- Check check = checkMode(remoteAddress, false);
- if (check != Check.ASK) {
- return check == Check.ALLOW;
- }
- CredentialsProvider provider = getCredentialsProvider();
- InetSocketAddress remote = (InetSocketAddress) remoteAddress;
- // Ask the user
- String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
- serverKey);
- String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
- String keyAlgorithm = serverKey.getAlgorithm();
- String remoteHost = remote.getHostString();
- URIish uri = JGitUserInteraction.toURI(session.getUsername(),
- remote);
- String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
- return askUser(provider, uri, prompt, //
- format(SshdText.get().knownHostsUnknownKeyMsg,
- remoteHost),
- format(SshdText.get().knownHostsKeyFingerprints,
- keyAlgorithm),
- md5, sha256);
- }
-
- public ModifiedKeyHandling acceptModifiedServerKey(
- SocketAddress 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);
- List<String> messages = new ArrayList<>();
- String warning = format(
- SshdText.get().knownHostsModifiedKeyWarning,
- keyAlgorithm, expected.getAlgorithm(), remoteHost,
- KeyUtils.getFingerPrint(BuiltinDigests.md5, expected),
- KeyUtils.getFingerPrint(BuiltinDigests.sha256, expected),
- KeyUtils.getFingerPrint(BuiltinDigests.md5, actual),
- 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(
- SshdText.get().knownHostsModifiedKeyDenyMsg, path));
- askUser(provider, uri, null,
- messages.toArray(new String[0]));
- }
- return ModifiedKeyHandling.DENY;
- }
- // ASK -- two questions: procceed? and store?
- List<CredentialItem> items = new ArrayList<>(messages.size() + 2);
- for (String message : messages) {
- items.add(new CredentialItem.InformationalMessage(message));
- }
- CredentialItem.YesNoType proceed = new CredentialItem.YesNoType(
- SshdText.get().knownHostsModifiedKeyAcceptPrompt);
- CredentialItem.YesNoType store = new CredentialItem.YesNoType(
- SshdText.get().knownHostsModifiedKeyStorePrompt);
- items.add(proceed);
- items.add(store);
- if (provider.get(uri, items) && proceed.getValue()) {
- return store.getValue() ? ModifiedKeyHandling.ALLOW_AND_STORE
- : ModifiedKeyHandling.ALLOW;
- }
- return ModifiedKeyHandling.DENY;
- }
-
- public boolean createNewFile(Path path) {
- CredentialsProvider provider = getCredentialsProvider();
- if (provider == null) {
- // We can't ask, so don't create the file
- return false;
- }
- URIish uri = new URIish().setPath(path.toString());
- return askUser(provider, uri, //
- format(SshdText.get().knownHostsUserAskCreationPrompt,
- path), //
- format(SshdText.get().knownHostsUserAskCreationMsg, path));
- }
- }
-
- private static class HostKeyFile extends ModifiableFileWatcher
- implements Supplier<List<HostEntryPair>> {
-
- private List<HostEntryPair> entries = Collections.emptyList();
-
- public HostKeyFile(Path path) {
- super(path);
- }
-
- @Override
- public List<HostEntryPair> get() {
- Path path = getPath();
- try {
- if (checkReloadRequired()) {
- if (!Files.exists(path)) {
- // Has disappeared.
- resetReloadAttributes();
- return Collections.emptyList();
- }
- LockFile lock = new LockFile(path.toFile());
- if (lock.lock()) {
- try {
- entries = reload(getPath());
- } finally {
- lock.unlock();
- }
- } else {
- LOG.warn(format(SshdText.get().knownHostsFileLockedRead,
- path));
- }
- }
- } catch (IOException e) {
- LOG.warn(format(SshdText.get().knownHostsFileReadFailed, path));
- }
- return Collections.unmodifiableList(entries);
- }
-
- private List<HostEntryPair> reload(Path path) throws IOException {
- try {
- List<KnownHostEntry> rawEntries = KnownHostEntryReader
- .readFromFile(path);
- updateReloadAttributes();
- if (rawEntries == null || rawEntries.isEmpty()) {
- return Collections.emptyList();
- }
- List<HostEntryPair> newEntries = new LinkedList<>();
- for (KnownHostEntry entry : rawEntries) {
- AuthorizedKeyEntry keyPart = entry.getKeyEntry();
- if (keyPart == null) {
- continue;
- }
- try {
- PublicKey serverKey = keyPart.resolvePublicKey(null,
- PublicKeyEntryResolver.IGNORING);
- if (serverKey == null) {
- LOG.warn(format(
- SshdText.get().knownHostsUnknownKeyType,
- path, entry.getConfigLine()));
- } else {
- newEntries.add(new HostEntryPair(entry, serverKey));
- }
- } catch (GeneralSecurityException e) {
- LOG.warn(format(SshdText.get().knownHostsInvalidLine,
- path, entry.getConfigLine()));
- }
- }
- return newEntries;
- } catch (FileNotFoundException e) {
- resetReloadAttributes();
- return Collections.emptyList();
- }
- }
- }
-
- private Collection<SshdSocketAddress> getCandidates(
- SocketAddress connectAddress, SocketAddress remoteAddress) {
- Collection<SshdSocketAddress> candidates = new TreeSet<>(
- SshdSocketAddress.BY_HOST_AND_PORT);
- candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress));
- candidates.add(SshdSocketAddress.toSshdSocketAddress(connectAddress));
- return candidates;
- }
-
- private String createHostKeyLine(Collection<SshdSocketAddress> patterns,
- PublicKey key) throws IOException {
- StringBuilder result = new StringBuilder();
- for (SshdSocketAddress address : patterns) {
- if (result.length() > 0) {
- result.append(',');
- }
- KnownHostHashValue.appendHostPattern(result, address.getHostName(),
- address.getPort());
- }
- result.append(' ');
- PublicKeyEntry.appendPublicKeyEntry(result, key);
- return result.toString();
- }
-
- private String updateHostKeyLine(String line, PublicKey newKey)
- throws IOException {
- // Replaces an existing public key by the new key
- int pos = line.indexOf(' ');
- if (pos > 0 && line.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
- // We're at the end of the marker. Skip ahead to the next blank.
- pos = line.indexOf(' ', pos + 1);
- }
- if (pos < 0) {
- // Don't update if bogus format
- return null;
- }
- StringBuilder result = new StringBuilder(line.substring(0, pos + 1));
- PublicKeyEntry.appendPublicKeyEntry(result, newKey);
- return result.toString();
- }
-
-}
--- /dev/null
+/*
+ * Copyright (C) 2019 Thomas Wolf <thomas.wolf@paranor.ch>
+ * 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.
+ * <p>
+ * 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}.
+ * </p>
+ *
+ * @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<PublicKey> 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<String> getUserKnownHostsFiles();
+
+ /**
+ * Retrieves the list of file names from the "GlobalKnownHostsFile" ssh
+ * config.
+ *
+ * @return the list as configured, with ~ already replaced
+ */
+ List<String> 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();
+ }
+}
/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * Copyright (C) 2018, 2019 Thomas Wolf <thomas.wolf@paranor.ch>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
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;
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;
private final Map<Tuple, HostConfigEntryResolver> defaultHostConfigEntryResolver = new ConcurrentHashMap<>();
- private final Map<Tuple, ServerKeyVerifier> defaultServerKeyVerifier = new ConcurrentHashMap<>();
+ private final Map<Tuple, ServerKeyDatabase> defaultServerKeyDatabase = new ConcurrentHashMap<>();
private final Map<Tuple, Iterable<KeyPair>> defaultKeys = new ConcurrentHashMap<>();
.filePasswordProvider(
createFilePasswordProvider(passphrases))
.hostConfigEntryResolver(configFile)
- .serverKeyVerifier(getServerKeyVerifier(home, sshDir))
+ .serverKeyVerifier(new JGitServerKeyVerifier(
+ getServerKeyDatabase(home, sshDir)))
.compressionFactories(
new ArrayList<>(BuiltinCompressions.VALUES))
.build();
}
/**
- * 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
* 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() {