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 <thomas.wolf@paranor.ch>
This commit is contained in:
Thomas Wolf 2019-06-20 19:43:49 +02:00
parent 8c74a54315
commit 124fbbc33a
6 changed files with 688 additions and 89 deletions

View File

@ -7,7 +7,14 @@ Bundle-Version: 5.5.0.qualifier
Bundle-Vendor: %Bundle-Vendor Bundle-Vendor: %Bundle-Vendor
Bundle-Localization: plugin Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: org.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.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;version="[5.5.0,5.6.0)",
org.eclipse.jgit.junit.ssh;version="[5.5.0,5.6.0)", org.eclipse.jgit.junit.ssh;version="[5.5.0,5.6.0)",

View File

@ -0,0 +1,221 @@
/*
* 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());
}
}

View File

@ -0,0 +1,182 @@
/*
* 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();
}
}
}

View File

@ -1,5 +1,5 @@
/* /*
* 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. * and other copyright owners as documented in the project's IP log.
* *
* This program and the accompanying materials are made available * This program and the accompanying materials are made available
@ -64,17 +64,15 @@
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier; 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.KnownHostEntry;
import org.apache.sshd.client.config.hosts.KnownHostHashValue; import org.apache.sshd.client.config.hosts.KnownHostHashValue;
import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair; import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.config.keys.KeyUtils;
@ -83,11 +81,12 @@
import org.apache.sshd.common.digest.BuiltinDigests; import org.apache.sshd.common.digest.BuiltinDigests;
import org.apache.sshd.common.util.io.ModifiableFileWatcher; import org.apache.sshd.common.util.io.ModifiableFileWatcher;
import org.apache.sshd.common.util.net.SshdSocketAddress; import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.internal.storage.file.LockFile; import org.eclipse.jgit.internal.storage.file.LockFile;
import org.eclipse.jgit.transport.CredentialItem; import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshConstants;
import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -149,14 +148,14 @@
* @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
* ssh-config</a> * ssh-config</a>
*/ */
public class OpenSshServerKeyVerifier public class OpenSshServerKeyDatabase
implements ServerKeyVerifier, ServerKeyLookup { implements ServerKeyDatabase {
// TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these // TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these
// files may be large! // files may be large!
private static final Logger LOG = LoggerFactory private static final Logger LOG = LoggerFactory
.getLogger(OpenSshServerKeyVerifier.class); .getLogger(OpenSshServerKeyDatabase.class);
/** Can be used to mark revoked known host lines. */ /** Can be used to mark revoked known host lines. */
private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$ private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$
@ -168,7 +167,7 @@ public class OpenSshServerKeyVerifier
private final List<HostKeyFile> defaultFiles = new ArrayList<>(); private final List<HostKeyFile> defaultFiles = new ArrayList<>();
/** /**
* Creates a new {@link OpenSshServerKeyVerifier}. * Creates a new {@link OpenSshServerKeyDatabase}.
* *
* @param askAboutNewFile * @param askAboutNewFile
* whether to ask the user, if possible, about creating a new * 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 * empty or {@code null}, in which case no default files are
* installed. The files need not exist. * installed. The files need not exist.
*/ */
public OpenSshServerKeyVerifier(boolean askAboutNewFile, public OpenSshServerKeyDatabase(boolean askAboutNewFile,
List<Path> defaultFiles) { List<Path> defaultFiles) {
if (defaultFiles != null) { if (defaultFiles != null) {
for (Path file : defaultFiles) { for (Path file : defaultFiles) {
@ -190,31 +189,24 @@ public OpenSshServerKeyVerifier(boolean askAboutNewFile,
this.askAboutNewFile = askAboutNewFile; this.askAboutNewFile = askAboutNewFile;
} }
private List<HostKeyFile> getFilesToUse(ClientSession session) { private List<HostKeyFile> getFilesToUse(@NonNull Configuration config) {
List<HostKeyFile> filesToUse = defaultFiles; List<HostKeyFile> filesToUse = defaultFiles;
if (session instanceof JGitClientSession) { List<HostKeyFile> userFiles = addUserHostKeyFiles(
HostConfigEntry entry = ((JGitClientSession) session) config.getUserKnownHostsFiles());
.getHostConfigEntry(); if (!userFiles.isEmpty()) {
if (entry instanceof JGitHostConfigEntry) { filesToUse = userFiles;
// Always true!
List<HostKeyFile> userFiles = addUserHostKeyFiles(
((JGitHostConfigEntry) entry).getMultiValuedOptions()
.get(SshConstants.USER_KNOWN_HOSTS_FILE));
if (!userFiles.isEmpty()) {
filesToUse = userFiles;
}
}
} }
return filesToUse; return filesToUse;
} }
@Override @Override
public List<PublicKey> lookup(ClientSession session, public List<PublicKey> lookup(@NonNull String connectAddress,
SocketAddress remote) { @NonNull InetSocketAddress remoteAddress,
List<HostKeyFile> filesToUse = getFilesToUse(session); @NonNull Configuration config) {
List<HostKeyFile> filesToUse = getFilesToUse(config);
List<PublicKey> result = new ArrayList<>(); List<PublicKey> result = new ArrayList<>();
Collection<SshdSocketAddress> candidates = getCandidates( Collection<SshdSocketAddress> candidates = getCandidates(
session.getConnectAddress(), remote); connectAddress, remoteAddress);
for (HostKeyFile file : filesToUse) { for (HostKeyFile file : filesToUse) {
for (HostEntryPair current : file.get()) { for (HostEntryPair current : file.get()) {
KnownHostEntry entry = current.getHostEntry(); KnownHostEntry entry = current.getHostEntry();
@ -230,14 +222,16 @@ public List<PublicKey> lookup(ClientSession session,
} }
@Override @Override
public boolean verifyServerKey(ClientSession clientSession, public boolean accept(@NonNull String connectAddress,
SocketAddress remoteAddress, PublicKey serverKey) { @NonNull InetSocketAddress remoteAddress,
List<HostKeyFile> filesToUse = getFilesToUse(clientSession); @NonNull PublicKey serverKey,
AskUser ask = new AskUser(clientSession); @NonNull Configuration config, CredentialsProvider provider) {
List<HostKeyFile> filesToUse = getFilesToUse(config);
AskUser ask = new AskUser(config, provider);
HostEntryPair[] modified = { null }; HostEntryPair[] modified = { null };
Path path = null; Path path = null;
Collection<SshdSocketAddress> candidates = getCandidates( Collection<SshdSocketAddress> candidates = getCandidates(connectAddress,
clientSession.getConnectAddress(), remoteAddress); remoteAddress);
for (HostKeyFile file : filesToUse) { for (HostKeyFile file : filesToUse) {
try { try {
if (find(candidates, serverKey, file.get(), modified)) { if (find(candidates, serverKey, file.get(), modified)) {
@ -433,16 +427,14 @@ private enum Check {
ASK, DENY, ALLOW; ASK, DENY, ALLOW;
} }
private final JGitClientSession session; private final @NonNull Configuration config;
public AskUser(ClientSession clientSession) { private final CredentialsProvider provider;
session = (clientSession instanceof JGitClientSession)
? (JGitClientSession) clientSession
: null;
}
private CredentialsProvider getCredentialsProvider() { public AskUser(@NonNull Configuration config,
return session == null ? null : session.getCredentialsProvider(); CredentialsProvider provider) {
this.config = config;
this.provider = provider;
} }
private static boolean askUser(CredentialsProvider provider, URIish uri, private static boolean askUser(CredentialsProvider provider, URIish uri,
@ -465,38 +457,25 @@ private Check checkMode(SocketAddress remoteAddress, boolean changed) {
if (!(remoteAddress instanceof InetSocketAddress)) { if (!(remoteAddress instanceof InetSocketAddress)) {
return Check.DENY; return Check.DENY;
} }
HostConfigEntry entry = session.getHostConfigEntry(); switch (config.getStrictHostKeyChecking()) {
String value = entry case REQUIRE_MATCH:
.getProperty(SshConstants.STRICT_HOST_KEY_CHECKING, "ask"); //$NON-NLS-1$
switch (value.toLowerCase(Locale.ROOT)) {
case SshConstants.YES:
case SshConstants.ON:
return Check.DENY; return Check.DENY;
case SshConstants.NO: case ACCEPT_ANY:
case SshConstants.OFF:
return Check.ALLOW; return Check.ALLOW;
case "accept-new": //$NON-NLS-1$ case ACCEPT_NEW:
return changed ? Check.DENY : Check.ALLOW; return changed ? Check.DENY : Check.ALLOW;
default: default:
break; return provider == null ? Check.DENY : Check.ASK;
} }
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, public void revokedKey(SocketAddress remoteAddress, PublicKey serverKey,
Path path) { Path path) {
CredentialsProvider provider = getCredentialsProvider();
if (provider == null) { if (provider == null) {
return; return;
} }
InetSocketAddress remote = (InetSocketAddress) remoteAddress; InetSocketAddress remote = (InetSocketAddress) remoteAddress;
URIish uri = JGitUserInteraction.toURI(session.getUsername(), URIish uri = JGitUserInteraction.toURI(config.getUsername(),
remote); remote);
String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256, String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
serverKey); serverKey);
@ -516,7 +495,6 @@ public boolean acceptUnknownKey(SocketAddress remoteAddress,
if (check != Check.ASK) { if (check != Check.ASK) {
return check == Check.ALLOW; return check == Check.ALLOW;
} }
CredentialsProvider provider = getCredentialsProvider();
InetSocketAddress remote = (InetSocketAddress) remoteAddress; InetSocketAddress remote = (InetSocketAddress) remoteAddress;
// Ask the user // Ask the user
String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256, String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
@ -524,7 +502,7 @@ public boolean acceptUnknownKey(SocketAddress remoteAddress,
String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey); String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
String keyAlgorithm = serverKey.getAlgorithm(); String keyAlgorithm = serverKey.getAlgorithm();
String remoteHost = remote.getHostString(); String remoteHost = remote.getHostString();
URIish uri = JGitUserInteraction.toURI(session.getUsername(), URIish uri = JGitUserInteraction.toURI(config.getUsername(),
remote); remote);
String prompt = SshdText.get().knownHostsUnknownKeyPrompt; String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
return askUser(provider, uri, prompt, // return askUser(provider, uri, prompt, //
@ -536,18 +514,17 @@ public boolean acceptUnknownKey(SocketAddress remoteAddress,
} }
public ModifiedKeyHandling acceptModifiedServerKey( public ModifiedKeyHandling acceptModifiedServerKey(
SocketAddress remoteAddress, PublicKey expected, InetSocketAddress remoteAddress, PublicKey expected,
PublicKey actual, Path path) { PublicKey actual, Path path) {
Check check = checkMode(remoteAddress, true); Check check = checkMode(remoteAddress, true);
if (check == Check.ALLOW) { if (check == Check.ALLOW) {
// Never auto-store on CHECK.ALLOW // Never auto-store on CHECK.ALLOW
return ModifiedKeyHandling.ALLOW; return ModifiedKeyHandling.ALLOW;
} }
InetSocketAddress remote = (InetSocketAddress) remoteAddress;
String keyAlgorithm = actual.getAlgorithm(); String keyAlgorithm = actual.getAlgorithm();
String remoteHost = remote.getHostString(); String remoteHost = remoteAddress.getHostString();
URIish uri = JGitUserInteraction.toURI(session.getUsername(), URIish uri = JGitUserInteraction.toURI(config.getUsername(),
remote); remoteAddress);
List<String> messages = new ArrayList<>(); List<String> messages = new ArrayList<>();
String warning = format( String warning = format(
SshdText.get().knownHostsModifiedKeyWarning, SshdText.get().knownHostsModifiedKeyWarning,
@ -558,7 +535,6 @@ public ModifiedKeyHandling acceptModifiedServerKey(
KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual)); KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual));
messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$ messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$
CredentialsProvider provider = getCredentialsProvider();
if (check == Check.DENY) { if (check == Check.DENY) {
if (provider != null) { if (provider != null) {
messages.add(format( messages.add(format(
@ -587,7 +563,6 @@ public ModifiedKeyHandling acceptModifiedServerKey(
} }
public boolean createNewFile(Path path) { public boolean createNewFile(Path path) {
CredentialsProvider provider = getCredentialsProvider();
if (provider == null) { if (provider == null) {
// We can't ask, so don't create the file // We can't ask, so don't create the file
return false; return false;
@ -674,12 +649,56 @@ private List<HostEntryPair> reload(Path path) throws IOException {
} }
} }
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( private Collection<SshdSocketAddress> getCandidates(
SocketAddress connectAddress, SocketAddress remoteAddress) { @NonNull String connectAddress,
@NonNull InetSocketAddress remoteAddress) {
Collection<SshdSocketAddress> candidates = new TreeSet<>( Collection<SshdSocketAddress> candidates = new TreeSet<>(
SshdSocketAddress.BY_HOST_AND_PORT); SshdSocketAddress.BY_HOST_AND_PORT);
candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress)); candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress));
candidates.add(SshdSocketAddress.toSshdSocketAddress(connectAddress)); SshdSocketAddress address = toSshdSocketAddress(connectAddress);
if (address != null) {
candidates.add(address);
}
return candidates; return candidates;
} }

View File

@ -0,0 +1,169 @@
/*
* 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();
}
}

View File

@ -1,5 +1,5 @@
/* /*
* 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. * and other copyright owners as documented in the project's IP log.
* *
* This program and the accompanying materials are made available * This program and the accompanying materials are made available
@ -66,7 +66,6 @@
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory; import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory; import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver; 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.NamedFactory;
import org.apache.sshd.common.compression.BuiltinCompressions; import org.apache.sshd.common.compression.BuiltinCompressions;
import org.apache.sshd.common.config.keys.FilePasswordProvider; import org.apache.sshd.common.config.keys.FilePasswordProvider;
@ -77,10 +76,11 @@
import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider; import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory; import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory; 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.JGitSshClient;
import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig; import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction; 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.PasswordProviderWrapper;
import org.eclipse.jgit.internal.transport.sshd.SshdText; import org.eclipse.jgit.internal.transport.sshd.SshdText;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
@ -104,7 +104,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
private final Map<Tuple, HostConfigEntryResolver> defaultHostConfigEntryResolver = new ConcurrentHashMap<>(); 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<>(); private final Map<Tuple, Iterable<KeyPair>> defaultKeys = new ConcurrentHashMap<>();
@ -226,7 +226,8 @@ public SshdSession getSession(URIish uri,
.filePasswordProvider( .filePasswordProvider(
createFilePasswordProvider(passphrases)) createFilePasswordProvider(passphrases))
.hostConfigEntryResolver(configFile) .hostConfigEntryResolver(configFile)
.serverKeyVerifier(getServerKeyVerifier(home, sshDir)) .serverKeyVerifier(new JGitServerKeyVerifier(
getServerKeyDatabase(home, sshDir)))
.compressionFactories( .compressionFactories(
new ArrayList<>(BuiltinCompressions.VALUES)) new ArrayList<>(BuiltinCompressions.VALUES))
.build(); .build();
@ -380,28 +381,28 @@ protected File getSshConfig(@NonNull File sshDir) {
} }
/** /**
* Obtain a {@link ServerKeyVerifier} to read known_hosts files and to * Obtain a {@link ServerKeyDatabase} to verify server host keys. The
* verify server host keys. The default implementation returns a * default implementation returns a {@link ServerKeyDatabase} that
* {@link ServerKeyVerifier} that recognizes the two openssh standard files * recognizes the two openssh standard files {@code ~/.ssh/known_hosts} and
* {@code ~/.ssh/known_hosts} and {@code ~/.ssh/known_hosts2} as well as any * {@code ~/.ssh/known_hosts2} as well as any files configured via the
* files configured via the {@code UserKnownHostsFile} option in the ssh * {@code UserKnownHostsFile} option in the ssh config file.
* config file.
* *
* @param homeDir * @param homeDir
* home directory to use for ~ replacement * home directory to use for ~ replacement
* @param sshDir * @param sshDir
* representing ~/.ssh/ * representing ~/.ssh/
* @return the resolver * @return the {@link ServerKeyDatabase}
* @since 5.5
*/ */
@NonNull @NonNull
private ServerKeyVerifier getServerKeyVerifier(@NonNull File homeDir, protected ServerKeyDatabase getServerKeyDatabase(@NonNull File homeDir,
@NonNull File sshDir) { @NonNull File sshDir) {
return defaultServerKeyVerifier.computeIfAbsent( return defaultServerKeyDatabase.computeIfAbsent(
new Tuple(new Object[] { homeDir, sshDir }), new Tuple(new Object[] { homeDir, sshDir }),
t -> new OpenSshServerKeyVerifier(true, t -> new OpenSshServerKeyDatabase(true,
getDefaultKnownHostsFiles(sshDir))); getDefaultKnownHostsFiles(sshDir)));
}
}
/** /**
* Gets the list of default user known hosts files. The default returns * Gets the list of default user known hosts files. The default returns
* ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config * ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config
@ -554,7 +555,7 @@ private List<NamedFactory<UserAuth>> getUserAuthFactories() {
* the ssh config defines {@code PreferredAuthentications} the value from * the ssh config defines {@code PreferredAuthentications} the value from
* the ssh config takes precedence. * 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 * none
*/ */
protected String getDefaultPreferredAuthentications() { protected String getDefaultPreferredAuthentications() {