diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java index 3c1111d24..97058e76e 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java @@ -44,6 +44,8 @@ package org.eclipse.jgit.junit.ssh; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyPair; @@ -101,6 +103,9 @@ public class SshTestGitServer { @NonNull private Repository repository; + @NonNull + private List hostKeys = new ArrayList<>(); + private final ExecutorService executorService = Executors .newFixedThreadPool(2); @@ -130,17 +135,16 @@ public class SshTestGitServer { this.repository = repository; server = SshServer.setUpDefaultServer(); // Set host key + try (ByteArrayInputStream in = new ByteArrayInputStream(hostKey)) { + hostKeys.add(SecurityUtils.loadKeyPairIdentity("", in, null)); + } catch (IOException | GeneralSecurityException e) { + // Ignore. + } server.setKeyPairProvider(new KeyPairProvider() { @Override public Iterable loadKeys() { - try (ByteArrayInputStream in = new ByteArrayInputStream( - hostKey)) { - return Collections.singletonList( - SecurityUtils.loadKeyPairIdentity("", in, null)); - } catch (IOException | GeneralSecurityException e) { - return null; - } + return hostKeys; } }); @@ -219,6 +223,32 @@ public class SshTestGitServer { return authentications; } + /** + * Adds an additional host key to the server. + * + * @param key + * path to the private key file; should not be encrypted + * @param inFront + * whether to add the new key before other existing keys + * @throws IOException + * if the file denoted by the {@link Path} {@code key} cannot be + * read + * @throws GeneralSecurityException + * if the key contained in the file cannot be read + */ + public void addHostKey(@NonNull Path key, boolean inFront) + throws IOException, GeneralSecurityException { + try (InputStream in = Files.newInputStream(key)) { + KeyPair pair = SecurityUtils.loadKeyPairIdentity(key.toString(), in, + null); + if (inFront) { + hostKeys.add(0, pair); + } else { + hostKeys.add(pair); + } + } + } + /** * Starts the test server, listening on a random port. * diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF index f79287d75..987f8dcdc 100644 --- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF @@ -69,6 +69,7 @@ Import-Package: org.apache.sshd.agent;version="[2.0.0,2.1.0)", org.apache.sshd.server.auth;version="[2.0.0,2.1.0)", org.eclipse.jgit.annotations;version="[5.2.0,5.3.0)", org.eclipse.jgit.errors;version="[5.2.0,5.3.0)", + org.eclipse.jgit.fnmatch;version="[5.2.0,5.3.0)", org.eclipse.jgit.internal.storage.file;version="[5.2.0,5.3.0)", org.eclipse.jgit.internal.transport.ssh;version="[5.2.0,5.3.0)", org.eclipse.jgit.nls;version="[5.2.0,5.3.0)", diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties index 0dc8ecc9a..369c9784d 100644 --- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties +++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties @@ -1,7 +1,10 @@ authenticationCanceled=Authentication canceled: no password closeListenerFailed=Ssh session close listener failed configInvalidPath=Invalid path in ssh config key {0}: {1} +configInvalidPattern=Invalid pattern in ssh config key {0}: {1} configInvalidPositive=Ssh config entry {0} must be a strictly positive number but is ''{1}'' +configNoKnownHostKeyAlgorithms=No implementations for any of the algorithms ''{0}'' given in HostKeyAlgorithms in the ssh config; using the default. +configNoRemainingHostKeyAlgorithms=Ssh config removed all host key algorithms: HostKeyAlgorithms ''{0}'' ftpCloseFailed=Closing the SFTP channel failed gssapiFailure=GSS-API error for mechanism OID {0} gssapiInitFailure=GSS-API initialization failure for mechanism {0} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java index 2bde7e711..3e2a1aa6d 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java @@ -42,11 +42,27 @@ */ package org.eclipse.jgit.internal.transport.sshd; +import static java.text.MessageFormat.format; + +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + import org.apache.sshd.client.ClientFactoryManager; import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.session.ClientSessionImpl; +import org.apache.sshd.common.FactoryManager; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.io.IoSession; +import org.eclipse.jgit.errors.InvalidPatternException; +import org.eclipse.jgit.fnmatch.FileNameMatcher; import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.SshConstants; /** * A {@link org.apache.sshd.client.session.ClientSession ClientSession} that can @@ -111,4 +127,103 @@ public class JGitClientSession extends ClientSessionImpl { return credentialsProvider; } + @Override + protected String resolveAvailableSignaturesProposal( + FactoryManager manager) { + Set defaultSignatures = new LinkedHashSet<>(); + defaultSignatures.addAll(getSignatureFactoriesNames()); + HostConfigEntry config = resolveAttribute( + JGitSshClient.HOST_CONFIG_ENTRY); + String hostKeyAlgorithms = config + .getProperty(SshConstants.HOST_KEY_ALGORITHMS); + if (hostKeyAlgorithms != null && !hostKeyAlgorithms.isEmpty()) { + char first = hostKeyAlgorithms.charAt(0); + if (first == '+') { + // Additions make not much sense -- it's either in + // defaultSignatures already, or we have no implementation for + // it. No point in proposing it. + return String.join(",", defaultSignatures); //$NON-NLS-1$ + } else if (first == '-') { + // This takes wildcard patterns! + removeFromList(defaultSignatures, + SshConstants.HOST_KEY_ALGORITHMS, + hostKeyAlgorithms.substring(1)); + if (defaultSignatures.isEmpty()) { + // Too bad: user config error. Warn here, and then fail + // later. + log.warn(format( + SshdText.get().configNoRemainingHostKeyAlgorithms, + hostKeyAlgorithms)); + } + return String.join(",", defaultSignatures); //$NON-NLS-1$ + } else { + // Default is overridden -- only accept the ones for which we do + // have an implementation. + List newNames = filteredList(defaultSignatures, + hostKeyAlgorithms); + if (newNames.isEmpty()) { + log.warn(format( + SshdText.get().configNoKnownHostKeyAlgorithms, + hostKeyAlgorithms)); + // Use the default instead. + } else { + return String.join(",", newNames); //$NON-NLS-1$ + } + } + } + // No HostKeyAlgorithms; using default -- change order to put existing + // keys first. + ServerKeyVerifier verifier = getServerKeyVerifier(); + if (verifier instanceof ServerKeyLookup) { + List allKnownKeys = ((ServerKeyLookup) verifier) + .lookup(this, this.getIoSession().getRemoteAddress()); + Set reordered = new LinkedHashSet<>(); + for (HostEntryPair h : allKnownKeys) { + PublicKey key = h.getServerKey(); + if (key != null) { + String keyType = KeyUtils.getKeyType(key); + if (keyType != null) { + reordered.add(keyType); + } + } + } + reordered.addAll(defaultSignatures); + return String.join(",", reordered); //$NON-NLS-1$ + } + return String.join(",", defaultSignatures); //$NON-NLS-1$ + } + + private void removeFromList(Set current, String key, + String patterns) { + for (String toRemove : patterns.split("\\s*,\\s*")) { //$NON-NLS-1$ + if (toRemove.indexOf('*') < 0 && toRemove.indexOf('?') < 0) { + current.remove(toRemove); + continue; + } + try { + FileNameMatcher matcher = new FileNameMatcher(toRemove, null); + for (Iterator i = current.iterator(); i.hasNext();) { + matcher.reset(); + matcher.append(i.next()); + if (matcher.isMatch()) { + i.remove(); + } + } + } catch (InvalidPatternException e) { + log.warn(format(SshdText.get().configInvalidPattern, key, + toRemove)); + } + } + } + + private List filteredList(Set known, String values) { + List newNames = new ArrayList<>(); + for (String newValue : values.split("\\s*,\\s*")) { //$NON-NLS-1$ + if (known.contains(newValue)) { + newNames.add(newValue); + } + } + return newNames; + } + } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java index 36e448623..27cf05077 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java @@ -83,6 +83,13 @@ import org.eclipse.jgit.transport.sshd.KeyCache; */ public class JGitSshClient extends SshClient { + /** + * We need access to this during the constructor of the ClientSession, + * before setConnectAddress() can have been called. So we have to remember + * it in an attribute on the SshClient, from where we can then retrieve it. + */ + static final AttributeKey HOST_CONFIG_ENTRY = new AttributeKey<>(); + /** * An attribute key for the comma-separated list of default preferred * authentication mechanisms. @@ -124,6 +131,7 @@ public class JGitSshClient extends SshClient { hostConfig.getProperty(SshConstants.PREFERRED_AUTHENTICATIONS, getAttribute(PREFERRED_AUTHENTICATIONS)), PREFERRED_AUTHS); + setAttribute(HOST_CONFIG_ENTRY, hostConfig); connector.connect(address).addListener(listener); return connectFuture; } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java new file mode 100644 index 000000000..4db24a16b --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2018, Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.sshd; + +import static java.text.MessageFormat.format; +import static org.apache.sshd.client.config.hosts.HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM; +import static org.apache.sshd.client.config.hosts.HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.sshd.client.config.hosts.HostPatternValue; +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.common.config.keys.AuthorizedKeyEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Apache MINA sshd 2.0.0 KnownHostEntry cannot read a host entry line like + * "host:port ssh-rsa "; it complains about an illegal character in the + * host name (correct would be "[host]:port"). The default known_hosts reader + * also aborts reading on the first error. + *

+ * This reader is a bit more robust and tries to handle this case if there is + * only one colon (otherwise it might be an IPv6 address (without port)), and it + * skips and logs invalid entries, but still returns all other valid entries + * from the file. + *

+ */ +public class KnownHostEntryReader { + + private static final Logger LOG = LoggerFactory + .getLogger(KnownHostEntryReader.class); + + private KnownHostEntryReader() { + // No instantiation + } + + /** + * Reads a known_hosts file and returns all valid entries. Invalid entries + * are skipped (and a message is logged). + * + * @param path + * of the file to read + * @return a {@link List} of all valid entries read from the file + * @throws IOException + * if the file cannot be read. + */ + public static List readFromFile(Path path) + throws IOException { + List result = new LinkedList<>(); + try (BufferedReader r = Files.newBufferedReader(path, + StandardCharsets.UTF_8)) { + r.lines().forEachOrdered(l -> { + if (l == null) { + return; + } + String line = clean(l); + if (line.isEmpty()) { + return; + } + try { + KnownHostEntry entry = parseHostEntry(line); + if (entry != null) { + result.add(entry); + } else { + LOG.warn(format(SshdText.get().knownHostsInvalidLine, + path, line)); + } + } catch (RuntimeException e) { + LOG.warn(format(SshdText.get().knownHostsInvalidLine, path, + line), e); + } + }); + } + return result; + } + + private static String clean(String line) { + int i = line.indexOf('#'); + return i < 0 ? line.trim() : line.substring(0, i).trim(); + } + + private static KnownHostEntry parseHostEntry(String line) { + KnownHostEntry entry = new KnownHostEntry(); + entry.setConfigLine(line); + String tmp = line; + int i = 0; + if (tmp.charAt(0) == KnownHostEntry.MARKER_INDICATOR) { + // A marker + i = tmp.indexOf(' ', 1); + if (i < 0) { + return null; + } + entry.setMarker(tmp.substring(1, i)); + tmp = tmp.substring(i + 1).trim(); + } + i = tmp.indexOf(' '); + if (i < 0) { + return null; + } + // Hash, or host patterns + if (tmp.charAt(0) == KnownHostHashValue.HASHED_HOST_DELIMITER) { + // Hashed host entry + KnownHostHashValue hash = KnownHostHashValue + .parse(tmp.substring(0, i)); + if (hash == null) { + return null; + } + entry.setHashedEntry(hash); + entry.setPatterns(null); + } else { + Collection patterns = parsePatterns( + tmp.substring(0, i)); + if (patterns == null || patterns.isEmpty()) { + return null; + } + entry.setHashedEntry(null); + entry.setPatterns(patterns); + } + tmp = tmp.substring(i + 1).trim(); + AuthorizedKeyEntry key = AuthorizedKeyEntry + .parseAuthorizedKeyEntry(tmp); + if (key == null) { + return null; + } + entry.setKeyEntry(key); + return entry; + } + + private static Collection parsePatterns(String text) { + if (text.isEmpty()) { + return null; + } + List items = Arrays.stream(text.split(",")) //$NON-NLS-1$ + .filter(item -> item != null && !item.isEmpty()).map(item -> { + if (NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == item + .charAt(0)) { + return item; + } + int firstColon = item.indexOf(':'); + if (firstColon < 0) { + return item; + } + int secondColon = item.indexOf(':', firstColon + 1); + if (secondColon > 0) { + // Assume an IPv6 address (without port). + return item; + } + // We have "host:port", should be "[host]:port" + return NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM + + item.substring(0, firstColon) + + NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM + + item.substring(firstColon); + }).collect(Collectors.toList()); + return items.isEmpty() ? null : HostPatternsHolder.parsePatterns(items); + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java index a20ee6bb8..e511be01d 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java @@ -148,7 +148,8 @@ import org.slf4j.LoggerFactory; * @see man * ssh-config */ -public class OpenSshServerKeyVerifier implements ServerKeyVerifier { +public class OpenSshServerKeyVerifier + implements ServerKeyVerifier, ServerKeyLookup { // TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these // files may be large! @@ -192,12 +193,10 @@ public class OpenSshServerKeyVerifier implements ServerKeyVerifier { this.askAboutNewFile = askAboutNewFile; } - @Override - public boolean verifyServerKey(ClientSession clientSession, - SocketAddress remoteAddress, PublicKey serverKey) { + private List getFilesToUse(ClientSession session) { List filesToUse = defaultFiles; - if (clientSession instanceof JGitClientSession) { - HostConfigEntry entry = ((JGitClientSession) clientSession) + if (session instanceof JGitClientSession) { + HostConfigEntry entry = ((JGitClientSession) session) .getHostConfigEntry(); if (entry instanceof JGitHostConfigEntry) { // Always true! @@ -209,6 +208,35 @@ public class OpenSshServerKeyVerifier implements ServerKeyVerifier { } } } + return filesToUse; + } + + @Override + public List lookup(ClientSession session, + SocketAddress remote) { + List filesToUse = getFilesToUse(session); + HostKeyHelper helper = new HostKeyHelper(); + List result = new ArrayList<>(); + Collection candidates = helper + .resolveHostNetworkIdentities(session, 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); + break; + } + } + } + } + return result; + } + + @Override + public boolean verifyServerKey(ClientSession clientSession, + SocketAddress remoteAddress, PublicKey serverKey) { + List filesToUse = getFilesToUse(clientSession); AskUser ask = new AskUser(); HostEntryPair[] modified = { null }; Path path = null; @@ -634,8 +662,8 @@ public class OpenSshServerKeyVerifier implements ServerKeyVerifier { private List reload(Path path) throws IOException { try { - List rawEntries = KnownHostEntry - .readKnownHostEntries(path); + List rawEntries = KnownHostEntryReader + .readFromFile(path); updateReloadAttributes(); if (rawEntries == null || rawEntries.isEmpty()) { return Collections.emptyList(); @@ -652,13 +680,13 @@ public class OpenSshServerKeyVerifier implements ServerKeyVerifier { if (serverKey == null) { LOG.warn(format( SshdText.get().knownHostsUnknownKeyType, - getPath(), entry.getConfigLine())); + path, entry.getConfigLine())); } else { newEntries.add(new HostEntryPair(entry, serverKey)); } } catch (GeneralSecurityException e) { LOG.warn(format(SshdText.get().knownHostsInvalidLine, - getPath(), entry.getConfigLine())); + path, entry.getConfigLine())); } } return newEntries; diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/ServerKeyLookup.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/ServerKeyLookup.java new file mode 100644 index 000000000..4f5f497f7 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/ServerKeyLookup.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018, Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.sshd; + +import java.net.SocketAddress; +import java.util.List; + +import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair; +import org.apache.sshd.client.session.ClientSession; +import org.eclipse.jgit.annotations.NonNull; + +/** + * Offers operations to retrieve server keys from known_hosts files. + */ +public interface ServerKeyLookup { + + /** + * Retrieves all entries for a given remote address. + * + * @param session + * needed to determine the config files if specified in the ssh + * config + * @param remote + * to find entries for + * @return a possibly empty list of entries found, including revoked ones + */ + @NonNull + List lookup(ClientSession session, SocketAddress remote); +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java index 865a8ebaa..e7e5d8fcc 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java @@ -21,7 +21,10 @@ public final class SshdText extends TranslationBundle { /***/ public String authenticationCanceled; /***/ public String closeListenerFailed; /***/ public String configInvalidPath; + /***/ public String configInvalidPattern; /***/ public String configInvalidPositive; + /***/ public String configNoKnownHostKeyAlgorithms; + /***/ public String configNoRemainingHostKeyAlgorithms; /***/ public String ftpCloseFailed; /***/ public String gssapiFailure; /***/ public String gssapiInitFailure; diff --git a/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java b/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java index 3b5aa5adb..3e4493119 100644 --- a/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java +++ b/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java @@ -595,6 +595,42 @@ public abstract class SshTestBase extends SshTestHarness { "PreferredAuthentications password"); } + @Test + public void testRsaHostKeySecond() throws Exception { + // See https://git.eclipse.org/r/#/c/130402/ : server has EcDSA + // (preferred), RSA, we have RSA in known_hosts: client and server + // should agree on RSA. + File newHostKey = new File(getTemporaryDirectory(), "newhostkey"); + copyTestResource("id_ecdsa_256", newHostKey); + server.addHostKey(newHostKey.toPath(), true); + cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, // + "Host git", // + "HostName localhost", // + "Port " + testPort, // + "User " + TEST_USER, // + "IdentityFile " + privateKey1.getAbsolutePath()); + } + + @Test + public void testEcDsaHostKey() throws Exception { + // See https://git.eclipse.org/r/#/c/130402/ : server has RSA + // (preferred), EcDSA, we have EcDSA in known_hosts: client and server + // should agree on EcDSA. + File newHostKey = new File(getTemporaryDirectory(), "newhostkey"); + copyTestResource("id_ecdsa_256", newHostKey); + server.addHostKey(newHostKey.toPath(), false); + File newHostKeyPub = new File(getTemporaryDirectory(), + "newhostkey.pub"); + copyTestResource("id_ecdsa_256.pub", newHostKeyPub); + createKnownHostsFile(knownHosts, "localhost", testPort, newHostKeyPub); + cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, // + "Host git", // + "HostName localhost", // + "Port " + testPort, // + "User " + TEST_USER, // + "IdentityFile " + privateKey1.getAbsolutePath()); + } + @Theory public void testSshKeys(String keyName) throws Exception { // JSch fails on ECDSA 384/521 keys. Compare