This is an automated email from the ASF dual-hosted git repository.
twolf pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
The following commit(s) were added to refs/heads/master by this push:
new f1994bd3b [SSHD-1259] Match key type in known_hosts lookup (#368)
f1994bd3b is described below
commit f1994bd3bee631ae828def8fe9e9ab35310da8cd
Author: FliegenKLATSCH <[email protected]>
AuthorDate: Mon May 29 11:00:11 2023 +0200
[SSHD-1259] Match key type in known_hosts lookup (#368)
Consider key types, and consider all keys for a host. Also handle the
revoked flag: do not accept server keys that are marked as revoked in the
known_hosts files.
---
.../keyverifier/KnownHostsServerKeyVerifier.java | 106 ++++++------
.../keyverifier/ModifiedServerKeyAcceptor.java | 4 +-
.../KnownHostsServerKeyVerifierTest.java | 186 ++++++++++++++-------
.../org/apache/sshd/client/keyverifier/known_hosts | 3 +
4 files changed, 186 insertions(+), 113 deletions(-)
diff --git
a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java
b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java
index 97de87e5c..fe079fb2b 100644
---
a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java
+++
b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java
@@ -36,9 +36,11 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
import org.apache.sshd.client.config.hosts.KnownHostEntry;
import org.apache.sshd.client.config.hosts.KnownHostHashValue;
@@ -267,36 +269,63 @@ public class KnownHostsServerKeyVerifier
protected boolean acceptKnownHostEntries(
ClientSession clientSession, SocketAddress remoteAddress,
PublicKey serverKey,
Collection<HostEntryPair> knownHosts) {
- // TODO allow for several candidates and check if ANY of them matches
the key and has 'revoked' marker
- HostEntryPair match = findKnownHostEntry(clientSession, remoteAddress,
knownHosts);
- if (match == null) {
+
+ List<HostEntryPair> hostMatches = findKnownHostEntries(clientSession,
remoteAddress, knownHosts);
+ if (hostMatches.isEmpty()) {
return acceptUnknownHostKey(clientSession, remoteAddress,
serverKey);
}
- KnownHostEntry entry = match.getHostEntry();
- PublicKey expected = match.getServerKey();
- if (KeyUtils.compareKeys(expected, serverKey)) {
- return acceptKnownHostEntry(clientSession, remoteAddress,
serverKey, entry);
+ String serverKeyType = KeyUtils.getKeyType(serverKey);
+
+ List<HostEntryPair> keyMatches = hostMatches.stream()
+ .filter(entry ->
serverKeyType.equals(entry.getHostEntry().getKeyEntry().getKeyType()))
+ .filter(k -> KeyUtils.compareKeys(k.getServerKey(), serverKey))
+ .collect(Collectors.toList());
+
+ if (keyMatches.stream()
+ .anyMatch(k ->
"revoked".equals(k.getHostEntry().getMarker()))) {
+ log.debug("acceptKnownHostEntry({})[{}] key={}-{} marked as
revoked",
+ clientSession, remoteAddress,
KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey));
+ return false;
}
+ if (!keyMatches.isEmpty()) {
+ return true;
+ }
+
+ Optional<HostEntryPair> anyNonRevokedMatch = hostMatches.stream()
+ .filter(k -> !"revoked".equals(k.getHostEntry().getMarker()))
+ .findAny();
+
+ if (!anyNonRevokedMatch.isPresent()) {
+ return acceptUnknownHostKey(clientSession, remoteAddress,
serverKey);
+ }
+
+ KnownHostEntry entry = anyNonRevokedMatch.get().getHostEntry();
+ PublicKey expected = anyNonRevokedMatch.get().getServerKey();
+
try {
- if (!acceptModifiedServerKey(clientSession, remoteAddress, entry,
expected, serverKey)) {
- return false;
+ if (acceptModifiedServerKey(clientSession, remoteAddress, entry,
expected, serverKey)) {
+ updateModifiedServerKey(clientSession, remoteAddress,
serverKey, knownHosts, anyNonRevokedMatch.get());
+ return true;
}
} catch (Throwable t) {
warn("acceptKnownHostEntries({})[{}] failed ({}) to accept
modified server key: {}",
clientSession, remoteAddress,
t.getClass().getSimpleName(), t.getMessage(), t);
- return false;
}
+ return false;
+ }
+
+ protected void updateModifiedServerKey(
+ ClientSession clientSession, SocketAddress remoteAddress,
PublicKey serverKey, Collection<HostEntryPair> knownHosts,
+ HostEntryPair match) {
Path file = getPath();
try {
updateModifiedServerKey(clientSession, remoteAddress, match,
serverKey, file, knownHosts);
} catch (Throwable t) {
handleModifiedServerKeyUpdateFailure(clientSession, remoteAddress,
match, serverKey, file, knownHosts, t);
}
-
- return true;
}
/**
@@ -460,74 +489,45 @@ public class KnownHostsServerKeyVerifier
clientSession, remoteAddress,
reason.getClass().getSimpleName(), match, reason.getMessage(), reason);
}
- /**
- * Invoked <U>after</U> known host entry located and keys match - by
default checks that entry has not been revoked
- *
- * @param clientSession The {@link ClientSession}
- * @param remoteAddress The remote host address
- * @param serverKey The presented server {@link PublicKey}
- * @param entry The {@link KnownHostEntry} value - if {@code
null} then no known matching host entry was
- * found - default will call
- * {@link #acceptUnknownHostKey(ClientSession,
SocketAddress, PublicKey)}
- * @return {@code true} if OK to accept the server
- */
- protected boolean acceptKnownHostEntry(
- ClientSession clientSession, SocketAddress remoteAddress,
PublicKey serverKey, KnownHostEntry entry) {
- if (entry == null) { // not really expected, but manage it
- return acceptUnknownHostKey(clientSession, remoteAddress,
serverKey);
- }
-
- if ("revoked".equals(entry.getMarker())) {
- log.debug("acceptKnownHostEntry({})[{}] key={}-{} marked as {}",
- clientSession, remoteAddress,
KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey),
- entry.getMarker());
- return false;
- }
-
- if (log.isDebugEnabled()) {
- log.debug("acceptKnownHostEntry({})[{}] matched key={}-{}",
- clientSession, remoteAddress,
KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey));
- }
- return true;
- }
-
- protected HostEntryPair findKnownHostEntry(
+ protected List<HostEntryPair> findKnownHostEntries(
ClientSession clientSession, SocketAddress remoteAddress,
Collection<HostEntryPair> knownHosts) {
if (GenericUtils.isEmpty(knownHosts)) {
- return null;
+ return Collections.emptyList();
}
Collection<SshdSocketAddress> candidates =
resolveHostNetworkIdentities(clientSession, remoteAddress);
boolean debugEnabled = log.isDebugEnabled();
if (debugEnabled) {
- log.debug("findKnownHostEntry({})[{}] host network identities: {}",
+ log.debug("findKnownHostEntries({})[{}] host network identities:
{}",
clientSession, remoteAddress, candidates);
}
if (GenericUtils.isEmpty(candidates)) {
- return null;
+ return Collections.emptyList();
}
- for (HostEntryPair match : knownHosts) {
- KnownHostEntry entry = match.getHostEntry();
+ List<HostEntryPair> matches = new ArrayList<>();
+ for (HostEntryPair line : knownHosts) {
+ KnownHostEntry entry = line.getHostEntry();
for (SshdSocketAddress host : candidates) {
try {
if (entry.isHostMatch(host.getHostName(), host.getPort()))
{
if (debugEnabled) {
- log.debug("findKnownHostEntry({})[{}] matched
host={} for entry={}",
+ log.debug("findKnownHostEntries({})[{}] matched
host={} for entry={}",
clientSession, remoteAddress, host, entry);
}
- return match;
+ matches.add(line);
+ break;
}
} catch (RuntimeException | Error e) {
- warn("findKnownHostEntry({})[{}] failed ({}) to check
host={} for entry={}: {}",
+ warn("findKnownHostEntries({})[{}] failed ({}) to check
host={} for entry={}: {}",
clientSession, remoteAddress,
e.getClass().getSimpleName(),
host, entry.getConfigLine(), e.getMessage(), e);
}
}
}
- return null; // no match found
+ return matches;
}
/**
diff --git
a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ModifiedServerKeyAcceptor.java
b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ModifiedServerKeyAcceptor.java
index 966a970ed..c3a27ef74 100644
---
a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ModifiedServerKeyAcceptor.java
+++
b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ModifiedServerKeyAcceptor.java
@@ -35,8 +35,8 @@ public interface ModifiedServerKeyAcceptor {
*
* @param clientSession The {@link ClientSession}
* @param remoteAddress The remote host address
- * @param entry The original {@link KnownHostEntry} whose key did
not match
- * @param expected The expected server {@link PublicKey}
+ * @param entry Any original {@link KnownHostEntry} whose key did
not match
+ * @param expected Any expected server {@link PublicKey}
* @param actual The presented server {@link PublicKey}
* @return {@code true} if accept the server key anyway
* @throws Exception if cannot process the request - equivalent to
{@code false} return value
diff --git
a/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java
b/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java
index 67cdba0c8..9c323b855 100644
---
a/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java
+++
b/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java
@@ -28,8 +28,10 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.KeyPair;
import java.security.PublicKey;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -50,7 +52,6 @@ import
org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
import org.apache.sshd.common.mac.Mac;
import org.apache.sshd.common.random.JceRandomFactory;
import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.util.test.BaseTestSupport;
@@ -70,8 +71,8 @@ import org.mockito.Mockito;
@Category({ NoIoTestCase.class })
public class KnownHostsServerKeyVerifierTest extends BaseTestSupport {
private static final String HASHED_HOST = "192.168.1.61";
- private static final Map<SshdSocketAddress, PublicKey> HOST_KEYS = new
TreeMap<>(SshdSocketAddress.BY_HOST_AND_PORT);
- private static Map<SshdSocketAddress, KnownHostEntry> hostsEntries;
+ private static final Map<SshdSocketAddress, List<PublicKey>> HOST_KEYS =
new TreeMap<>(SshdSocketAddress.BY_HOST_AND_PORT);
+ private static Map<SshdSocketAddress, List<KnownHostEntry>> hostsEntries;
private static Path entriesFile;
public KnownHostsServerKeyVerifierTest() {
@@ -87,12 +88,13 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
hostsEntries = loadEntries(entriesFile);
// Cannot use forEach because of the potential
IOException/GeneralSecurityException being thrown
- for (Map.Entry<SshdSocketAddress, KnownHostEntry> ke :
hostsEntries.entrySet()) {
- SshdSocketAddress hostIdentity = ke.getKey();
- KnownHostEntry entry = ke.getValue();
- AuthorizedKeyEntry authEntry =
ValidateUtils.checkNotNull(entry.getKeyEntry(), "No key extracted from %s",
entry);
- PublicKey key = authEntry.resolvePublicKey(null,
Collections.emptyMap(), PublicKeyEntryResolver.FAILING);
- assertNull("Multiple keys for host=" + hostIdentity,
HOST_KEYS.put(hostIdentity, key));
+ for (Map.Entry<SshdSocketAddress, List<KnownHostEntry>> entry :
hostsEntries.entrySet()) {
+ for (KnownHostEntry knownHostEntry : entry.getValue()) {
+ AuthorizedKeyEntry authEntry
+ =
ValidateUtils.checkNotNull(knownHostEntry.getKeyEntry(), "No key extracted from
%s", entry.getKey());
+ PublicKey key = authEntry.resolvePublicKey(null,
Collections.emptyMap(), PublicKeyEntryResolver.FAILING);
+ HOST_KEYS.computeIfAbsent(entry.getKey(), k -> new
ArrayList<>()).add(key);
+ }
}
}
@@ -120,15 +122,22 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
ClientFactoryManager manager =
Mockito.mock(ClientFactoryManager.class);
Mockito.when(manager.getRandomFactory()).thenReturn((Factory)
JceRandomFactory.INSTANCE);
- HOST_KEYS.entrySet().parallelStream().forEach(line -> {
- KnownHostEntry entry = hostsEntries.get(line.getKey());
+ HOST_KEYS.forEach((host, list) -> list.forEach(publicKey -> {
+ KnownHostEntry entry = hostsEntries.get(host).stream()
+ .filter(key ->
key.getKeyEntry().getKeyType().equals(KeyUtils.getKeyType(publicKey)))
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("Missing
updated key for " + KeyUtils.getKeyType(publicKey)));
ClientSession session = Mockito.mock(ClientSession.class);
Mockito.when(session.getFactoryManager()).thenReturn(manager);
-
Mockito.when(session.getConnectAddress()).thenReturn(line.getKey());
- assertTrue("Failed to validate server=" + entry,
verifier.verifyServerKey(session, line.getKey(), line.getValue()));
- });
+ Mockito.when(session.getConnectAddress()).thenReturn(host);
+ if ("revoked".equals(entry.getMarker())) {
+ assertFalse("Failed to validate server=" + entry,
verifier.verifyServerKey(session, host, publicKey));
+ } else {
+ assertTrue("Failed to validate server=" + entry,
verifier.verifyServerKey(session, host, publicKey));
+ }
+ }));
}
@Test
@@ -154,13 +163,20 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
};
- HOST_KEYS.forEach((hostIdentity, serverKey) -> {
- KnownHostEntry entry = hostsEntries.get(hostIdentity);
+ HOST_KEYS.forEach((host, list) -> list.forEach(hostKey -> {
+ KnownHostEntry entry = hostsEntries.get(host).stream()
+ .filter(key ->
key.getKeyEntry().getKeyType().equals(KeyUtils.getKeyType(hostKey)))
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("Missing
updated key for " + KeyUtils.getKeyType(hostKey)));
outputDebugMessage("Verify host=%s", entry);
- assertTrue("Failed to verify server=" + entry,
invokeVerifier(verifier, hostIdentity, serverKey));
+ if ("revoked".equals(entry.getMarker())) {
+ assertFalse("Failed to verify server=" + entry,
invokeVerifier(verifier, host, hostKey));
+ } else {
+ assertTrue("Failed to verify server=" + entry,
invokeVerifier(verifier, host, hostKey));
+ }
assertEquals("Unexpected delegate invocation for host=" + entry,
0, delegateCount.get());
assertEquals("Unexpected update invocation for host=" + entry, 0,
updateCount.get());
- });
+ }));
}
@Test
@@ -188,10 +204,12 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
int verificationCount = 0;
// Cannot use forEach because the verification count variable is not
effectively final
- for (Map.Entry<SshdSocketAddress, PublicKey> ke :
HOST_KEYS.entrySet()) {
- SshdSocketAddress hostIdentity = ke.getKey();
- PublicKey serverKey = ke.getValue();
- KnownHostEntry entry = hostsEntries.get(hostIdentity);
+ for (SshdSocketAddress hostIdentity : HOST_KEYS.keySet()) {
+ PublicKey serverKey = HOST_KEYS.get(hostIdentity).get(0);
+ KnownHostEntry entry = hostsEntries.get(hostIdentity).stream()
+ .filter(key ->
key.getKeyEntry().getKeyType().equals(KeyUtils.getKeyType(serverKey)))
+ .findAny()
+ .orElseThrow(() -> new IllegalStateException("Missing
updated key for " + KeyUtils.getKeyType(serverKey)));
outputDebugMessage("Verify host=%s", entry);
assertTrue("Failed to verify server=" + entry,
invokeVerifier(verifier, hostIdentity, serverKey));
verificationCount++;
@@ -200,12 +218,21 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
}
// make sure we have all the original entries and ONLY them
- Map<SshdSocketAddress, KnownHostEntry> updatedEntries =
loadEntries(path);
- hostsEntries.forEach((hostIdentity, expected) -> {
- KnownHostEntry actual = updatedEntries.remove(hostIdentity);
- assertNotNull("No updated entry for host=" + hostIdentity, actual);
+ Map<SshdSocketAddress, List<KnownHostEntry>> updatedEntries =
loadEntries(path);
+ hostsEntries.keySet().forEach(hostIdentity -> {
+ KnownHostEntry expected = hostsEntries.get(hostIdentity).get(0);
+
+ KnownHostEntry actual = updatedEntries.get(hostIdentity).stream()
+ .filter(key ->
key.getKeyEntry().getKeyType().equals(expected.getKeyEntry().getKeyType()))
+ .findAny()
+ .orElseThrow(() -> new IllegalStateException("Missing
updated key for " + expected));
+
+ assertTrue("No updated entry for host=" + hostIdentity,
updatedEntries.get(hostIdentity).remove(actual));
+ if (updatedEntries.get(hostIdentity).isEmpty()) {
+ updatedEntries.remove(hostIdentity);
+ }
- String expLine = expected.getConfigLine();
+ String expLine = expected.getConfigLine().replace("@revoked ", "");
// if original is a list or hashed then replace them with the
expected host
if ((expLine.indexOf(',') > 0) ||
(expLine.indexOf(KnownHostHashValue.HASHED_HOST_DELIMITER) >= 0)) {
int pos = expLine.indexOf(' ');
@@ -242,12 +269,16 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
ClientSession session = Mockito.mock(ClientSession.class);
Mockito.when(session.getFactoryManager()).thenReturn(manager);
- HOST_KEYS.forEach((hostIdentity, serverKey) -> {
- KnownHostEntry entry = hostsEntries.get(hostIdentity);
+ HOST_KEYS.keySet().forEach(host -> {
+ PublicKey serverKey = HOST_KEYS.get(host).get(0);
+ KnownHostEntry entry = hostsEntries.get(host).stream()
+ .filter(key ->
key.getKeyEntry().getKeyType().equals(KeyUtils.getKeyType(serverKey)))
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("Missing
updated key for " + KeyUtils.getKeyType(serverKey)));
outputDebugMessage("Write host=%s", entry);
- Mockito.when(session.getConnectAddress()).thenReturn(hostIdentity);
- assertTrue("Failed to validate server=" + entry,
verifier.verifyServerKey(session, hostIdentity, serverKey));
+ Mockito.when(session.getConnectAddress()).thenReturn(host);
+ assertTrue("Failed to validate server=" + entry,
verifier.verifyServerKey(session, host, serverKey));
});
// force re-read to ensure all values are hashed
@@ -259,12 +290,16 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
verifier.setLoadedHostsEntries(keys);
// make sure can still validate the original hosts
- HOST_KEYS.forEach((hostIdentity, serverKey) -> {
- KnownHostEntry entry = hostsEntries.get(hostIdentity);
+ HOST_KEYS.keySet().forEach(host -> {
+ PublicKey serverKey = HOST_KEYS.get(host).get(0);
+ KnownHostEntry entry = hostsEntries.get(host).stream()
+ .filter(key ->
key.getKeyEntry().getKeyType().equals(KeyUtils.getKeyType(serverKey)))
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("Missing
updated key for " + KeyUtils.getKeyType(serverKey)));
outputDebugMessage("Re-validate host=%s", entry);
- Mockito.when(session.getConnectAddress()).thenReturn(hostIdentity);
- assertTrue("Failed to re-validate server=" + entry,
verifier.verifyServerKey(session, hostIdentity, serverKey));
+ Mockito.when(session.getConnectAddress()).thenReturn(host);
+ assertTrue("Failed to re-validate server=" + entry,
verifier.verifyServerKey(session, host, serverKey));
});
}
@@ -273,7 +308,8 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
KeyPair kp =
CommonTestSupportUtils.generateKeyPair(KeyUtils.RSA_ALGORITHM, 1024);
PublicKey modifiedKey = kp.getPublic();
AtomicInteger acceptCount = new AtomicInteger(0);
- ServerKeyVerifier verifier
+ AtomicInteger unknownCount = new AtomicInteger(0);
+ KnownHostsServerKeyVerifier verifier
= new
KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE,
createKnownHostsCopy()) {
@Override
public boolean acceptModifiedServerKey(
@@ -281,20 +317,41 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
KnownHostEntry entry, PublicKey expected,
PublicKey actual)
throws Exception {
acceptCount.incrementAndGet();
+ assertNull("Unexpected marker for " + remoteAddress,
entry.getMarker());
assertSame("Mismatched actual key for " +
remoteAddress, modifiedKey, actual);
return super.acceptModifiedServerKey(clientSession,
remoteAddress, entry, expected, actual);
}
+
+ @Override
+ protected boolean acceptUnknownHostKey(
+ ClientSession clientSession, SocketAddress
remoteAddress, PublicKey serverKey) {
+ unknownCount.incrementAndGet();
+ return false;
+ }
};
int validationCount = 0;
+ int validUnknownCount = 0;
// Cannot use forEach because the validation count variable is not
effectively final
- for (Map.Entry<SshdSocketAddress, KnownHostEntry> ke :
hostsEntries.entrySet()) {
+ for (Map.Entry<SshdSocketAddress, List<KnownHostEntry>> ke :
hostsEntries.entrySet()) {
SshdSocketAddress hostIdentity = ke.getKey();
- KnownHostEntry entry = ke.getValue();
- outputDebugMessage("Verify host=%s", entry);
- assertFalse("Unexpected to verification success for " + entry,
invokeVerifier(verifier, hostIdentity, modifiedKey));
- validationCount++;
- assertEquals("Mismatched invocation count for host=" + entry,
validationCount, acceptCount.get());
+ for (KnownHostEntry entry : ke.getValue()) {
+ outputDebugMessage("Verify host=%s", entry);
+ assertFalse("Unexpected to verification success for " + entry,
+ invokeVerifier(verifier, hostIdentity, modifiedKey));
+ long acceptedCount = ke.getValue().stream()
+ .filter(k -> !"revoked".equals(k.getMarker()))
+ .count();
+ if (acceptedCount == 0) {
+ validUnknownCount++;
+ } else {
+ validationCount++;
+ }
+ assertEquals("Mismatched invocation count
(acceptModifiedServerKey) for host=" + entry, validationCount,
+ acceptCount.get());
+ assertEquals("Mismatched invocation count
(acceptUnknownHostKey) host=" + entry, validUnknownCount,
+ unknownCount.get());
+ }
}
}
@@ -303,6 +360,7 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
KeyPair kp =
CommonTestSupportUtils.generateKeyPair(KeyUtils.RSA_ALGORITHM, 1024);
PublicKey modifiedKey = kp.getPublic();
Path path = createKnownHostsCopy();
+ Files.deleteIfExists(path);
ServerKeyVerifier verifier = new
KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, path) {
@Override
public boolean acceptModifiedServerKey(
@@ -314,18 +372,26 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
}
};
- hostsEntries.forEach((host, entry) -> {
- outputDebugMessage("Verify host=%s", entry);
- assertTrue("Failed to verify " + entry, invokeVerifier(verifier,
host, modifiedKey));
+ hostsEntries.forEach((host, list) -> {
+ outputDebugMessage("Verify host=%s", host);
+ assertTrue("Failed to verify " + host, invokeVerifier(verifier,
host, modifiedKey));
});
String expected = PublicKeyEntry.toString(modifiedKey);
- Map<SshdSocketAddress, KnownHostEntry> updatedKeys = loadEntries(path);
- hostsEntries.forEach((hostIdentity, original) -> {
- KnownHostEntry updated = updatedKeys.remove(hostIdentity);
- assertNotNull("No updated entry for " + original, updated);
+ Map<SshdSocketAddress, List<KnownHostEntry>> updatedKeys =
loadEntries(path);
+ hostsEntries.keySet().forEach(host -> {
+ KnownHostEntry updated = updatedKeys.get(host).stream()
+ .filter(key ->
key.getKeyEntry().getKeyType().equals(KeyUtils.getKeyType(modifiedKey)))
+ .findAny()
+ .orElseThrow(
+ () -> new IllegalStateException("Missing updated
key for " + KeyUtils.getKeyType(modifiedKey)));
+ updatedKeys.get(host).remove(updated);
+ if (updatedKeys.get(host).isEmpty()) {
+ updatedKeys.remove(host);
+ }
String actual = updated.getConfigLine();
+ assertNotNull("No updated entry for " + hostsEntries.get(host),
actual);
int pos = actual.indexOf(' ');
if (actual.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
for (pos++; pos < actual.length(); pos++) {
@@ -337,7 +403,7 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
}
actual = GenericUtils.trimToEmpty(actual.substring(pos + 1));
- assertEquals("Mismatched updated value for host=" + hostIdentity,
expected, actual);
+ assertEquals("Mismatched updated value for host=" + host,
expected, actual);
});
assertTrue("Unexpected extra updated entries: " + updatedKeys,
updatedKeys.isEmpty());
@@ -395,8 +461,8 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
boolean accepted2 = invokeVerifier(verifier, address2, serverKey2);
assertTrue("Accepted on port=" + port2 + " ?", accepted2);
- Map<SshdSocketAddress, KnownHostEntry> updatedKeys = loadEntries(path);
- assertEquals("Mismatched total entries count", 2,
MapEntryUtils.size(updatedKeys));
+ Map<SshdSocketAddress, List<KnownHostEntry>> updatedKeys =
loadEntries(path);
+ assertEquals("Mismatched total entries count", 2, updatedKeys.size());
}
private Path createKnownHostsCopy() throws IOException {
@@ -418,21 +484,25 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
return verifier.verifyServerKey(session, hostIdentity, serverKey);
}
- private static Map<SshdSocketAddress, KnownHostEntry> loadEntries(Path
file) throws IOException {
+ private static Map<SshdSocketAddress, List<KnownHostEntry>>
loadEntries(Path file) throws IOException {
Collection<KnownHostEntry> entries =
KnownHostEntry.readKnownHostEntries(file);
if (GenericUtils.isEmpty(entries)) {
return Collections.emptyMap();
}
- Map<SshdSocketAddress, KnownHostEntry> hostsMap = new
TreeMap<>(SshdSocketAddress.BY_HOST_AND_PORT);
+ Map<SshdSocketAddress, List<KnownHostEntry>> hostsMap = new
TreeMap<>(SshdSocketAddress.BY_HOST_AND_PORT);
for (KnownHostEntry entry : entries) {
String line = entry.getConfigLine();
outputDebugMessage("loadTestLines(%s) processing %s", file, line);
// extract hosts
- int pos = line.indexOf(' ');
- String patterns = line.substring(0, pos);
+ int markerOffset = 0;
+ if (line.startsWith("@")) {
+ markerOffset = line.indexOf(' ') + 1;
+ }
+ int pos = line.indexOf(' ', markerOffset);
+ String patterns = line.substring(markerOffset, pos);
if (entry.getHashedEntry() != null) {
- assertNull("Multiple hashed entries in file", hostsMap.put(new
SshdSocketAddress(HASHED_HOST, 0), entry));
+ hostsMap.computeIfAbsent(new SshdSocketAddress(HASHED_HOST,
0), k -> new ArrayList<>()).add(entry);
} else {
String[] addrs = GenericUtils.split(patterns, ',');
for (String a : addrs) {
@@ -444,7 +514,7 @@ public class KnownHostsServerKeyVerifierTest extends
BaseTestSupport {
port = Integer.parseInt(a.substring(pos + 2));
a = a.substring(1, pos);
}
- assertNull("Multiple entries for address=" + a,
hostsMap.put(new SshdSocketAddress(a, port), entry));
+ hostsMap.computeIfAbsent(new SshdSocketAddress(a, port), k
-> new ArrayList<>()).add(entry);
}
}
}
diff --git
a/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts
b/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts
index 50a2944b8..b232c3820 100644
---
a/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts
+++
b/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts
@@ -4,7 +4,10 @@ server.sshd.apache.org,10.23.222.240 ecdsa-sha2-nistp256
AAAAE2VjZHNhLXNoYTItbml
# comment
10.23.222.127 ecdsa-sha2-nistp256
AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCbZVVpqEHGLNWMqMeyU1VbWb91XteoamVcgpy4yxNVbZffb5IDdbo1ons/y9KAhcub6LZeLrvXzVUZbXCZiUkg=
+10.23.222.127 ssh-rsa
AAAAB3NzaC1yc2EAAAABIwAAAQEAvQK9DhBSStbi2CaZNOo5vHy9Nga/hLCnO19tL6i5U/5Nhh5Y8W+tL+AA8hqD/doLBnyaEv2xjfzECwKlStc9HWx6EcJ+9B1rA4+5HztuUEWxuozNnkvcScjTBBqEd7fPPt0INI+pSZRYa2InEBBUUHTt1YaDEXamM/j4RVKovEH7Efgq9VUti148ZG90/w9V5ZT1o6yhuOw9UbME/eHIbS2E9P/Gy33OhkAgTLyOfCZAdJiYvcvFXqNWSKVFx3H5hSolh9ppxVFVFj2hW6QtvgYTphLU0ccHOWTBd/UToG4Xd9GjgSoD1pAI9e0NwBOsUfiqSzO99wqpzs6erfwelQ==
10.163.4.61 ssh-rsa
AAAAB3NzaC1yc2EAAAABIwAAAQEAvQK9DhBSStbi2CaZNOo5vHy9Nga/hLCnO19tL6i5U/5Nhh5Y8W+tL+AA8hqD/doLBnyaEv2xjfzECwKlStc9HWx6EcJ+9B1rA4+5HztuUEWxuozNnkvcScjTBBqEd7fPPt0INI+pSZRYa2InEBBUUHTt1YaDEXamM/j4RVKovEH7Efgq9VUti148ZG90/w9V5ZT1o6yhuOw9UbME/eHIbS2E9P/Gy33OhkAgTLyOfCZAdJiYvcvFXqNWSKVFx3H5hSolh9ppxVFVFj2hW6QtvgYTphLU0ccHOWTBd/UToG4Xd9GjgSoD1pAI9e0NwBOsUfiqSzO99wqpzs6erfwelQ==
+@revoked 10.163.4.61 ecdsa-sha2-nistp256
AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCbZVVpqEHGLNWMqMeyU1VbWb91XteoamVcgpy4yxNVbZffb5IDdbo1ons/y9KAhcub6LZeLrvXzVUZbXCZiUkg=
+@revoked 10.163.4.62 ssh-rsa
AAAAB3NzaC1yc2EAAAABIwAAAQEAvQK9DhBSStbi2CaZNOo5vHy9Nga/hLCnO19tL6i5U/5Nhh5Y8W+tL+AA8hqD/doLBnyaEv2xjfzECwKlStc9HWx6EcJ+9B1rA4+5HztuUEWxuozNnkvcScjTBBqEd7fPPt0INI+pSZRYa2InEBBUUHTt1YaDEXamM/j4RVKovEH7Efgq9VUti148ZG90/w9V5ZT1o6yhuOw9UbME/eHIbS2E9P/Gy33OhkAgTLyOfCZAdJiYvcvFXqNWSKVFx3H5hSolh9ppxVFVFj2hW6QtvgYTphLU0ccHOWTBd/UToG4Xd9GjgSoD1pAI9e0NwBOsUfiqSzO99wqpzs6erfwelQ==
192.168.174.129 ecdsa-sha2-nistp256
AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJvwXQ0Wc7TdhmjaTdkrHJvatWrw/6t6W1Hgh2nTauN+rhsMKYbmQeFTnzsP1NctWzQXwsqIOGcXIMNVhT92jgQ=