oscerd commented on code in PR #27821:
URL: https://github.com/apache/camel/pull/27821#discussion_r4270979829
##########
components/camel-ssh/src/main/java/org/apache/camel/component/ssh/ResourceBasedSSHKeyVerifier.java:
##########
@@ -88,105 +147,143 @@ public boolean verifyServerKey(ClientSession
sshClientSession, SocketAddress rem
}
}
- private PublicKey findKeyForServerToken(InputStream knownHostsInputStream,
List<String> possibleTokens) {
- String knowHostsLines =
camelContext.getTypeConverter().convertTo(String.class, knownHostsInputStream);
- if (knowHostsLines == null) {
- log.warn("Could not read from the known_hosts file input stream");
- return null;
+ private boolean isSameKey(KnownHostsEntry entry, PublicKey serverKey) {
+ if (entry.key == null) {
+ log.warn("Unsupported key type {} in the known_hosts file {}",
entry.keyType, knownHostsResource);
+ return false;
}
+ return KeyUtils.compareKeys(entry.key, serverKey);
+ }
- for (String s : knowHostsLines.split("\n")) {
- String[] parts = s.split(" ");
- if (parts.length != 3) {
- log.warn("Found malformed entry in known_hosts file");
- continue;
- }
- String entry = parts[0];
- String key = parts[2];
- for (String serverToken : possibleTokens) {
- if (entry.contains(serverToken)) {
- try {
- return loadKey(key);
- } catch (NoSuchAlgorithmException |
InvalidKeySpecException e) {
- log.warn(String.format("Could not load key for server
token %s", entry), e);
- }
- }
- }
+ /*
+ * The name OpenSSH looks the server up by: the host name or address the
client connected to, as configured on the
+ * endpoint and never resolved, written host for port 22 and [host]:port
for another port.
+ */
+ private static String toKnownHostsName(SocketAddress remoteAddress) {
+ if (!(remoteAddress instanceof InetSocketAddress) && !(remoteAddress
instanceof SshdSocketAddress)) {
+ return null;
}
- return null;
+ SshdSocketAddress address =
SshdSocketAddress.toSshdSocketAddress(remoteAddress);
+ String host = address.getHostName().toLowerCase(Locale.ROOT);
Review Comment:
Fixed in a1d570a52eef. The producer and the consumer now pass the host
configured on the endpoint to the verifier, through a new constructor
`ResourceBasedSSHKeyVerifier(CamelContext, String, boolean, String host)`. The
existing constructors still use the connected address. The name is built from
that host as it is written, with the brackets of an IPv6 address removed, and
from the port the client connected to. So an endpoint configured with `::1` is
looked up as `[::1]:<port>`.
Tests:
- `looksUpTheHostAsConfiguredOnTheEndpoint`: `::1` and `[::1]`, the listed
key and another key, both values of `failOnUnknownHost`.
- `SshComponentKnownHostIPv6Test`: connects to the test server over `::1`
with the listed key and another key, with both values of `failOnUnknownHost`.
It skips itself where the IPv6 loopback address is not available.
On 370054641e57, the two cases you describe fail: the listed key is rejected
with `failOnUnknownHost=true`, and another key is accepted with `false`.
The connection test sets the host on the endpoint, because camel-ssh does
not accept an IPv6 address in the endpoint uri today: `ssh://user@[::1]:2222`
ends with an empty host, and SSHD fails with "No target host". That uri gap is
separate and pre-existing, so it is not part of this PR.
_Claude Code on behalf of Andrea Cosentino (@oscerd)_
##########
components/camel-ssh/src/main/java/org/apache/camel/component/ssh/ResourceBasedSSHKeyVerifier.java:
##########
@@ -88,105 +147,143 @@ public boolean verifyServerKey(ClientSession
sshClientSession, SocketAddress rem
}
}
- private PublicKey findKeyForServerToken(InputStream knownHostsInputStream,
List<String> possibleTokens) {
- String knowHostsLines =
camelContext.getTypeConverter().convertTo(String.class, knownHostsInputStream);
- if (knowHostsLines == null) {
- log.warn("Could not read from the known_hosts file input stream");
- return null;
+ private boolean isSameKey(KnownHostsEntry entry, PublicKey serverKey) {
+ if (entry.key == null) {
+ log.warn("Unsupported key type {} in the known_hosts file {}",
entry.keyType, knownHostsResource);
+ return false;
}
+ return KeyUtils.compareKeys(entry.key, serverKey);
+ }
- for (String s : knowHostsLines.split("\n")) {
- String[] parts = s.split(" ");
- if (parts.length != 3) {
- log.warn("Found malformed entry in known_hosts file");
- continue;
- }
- String entry = parts[0];
- String key = parts[2];
- for (String serverToken : possibleTokens) {
- if (entry.contains(serverToken)) {
- try {
- return loadKey(key);
- } catch (NoSuchAlgorithmException |
InvalidKeySpecException e) {
- log.warn(String.format("Could not load key for server
token %s", entry), e);
- }
- }
- }
+ /*
+ * The name OpenSSH looks the server up by: the host name or address the
client connected to, as configured on the
+ * endpoint and never resolved, written host for port 22 and [host]:port
for another port.
+ */
+ private static String toKnownHostsName(SocketAddress remoteAddress) {
+ if (!(remoteAddress instanceof InetSocketAddress) && !(remoteAddress
instanceof SshdSocketAddress)) {
+ return null;
}
- return null;
+ SshdSocketAddress address =
SshdSocketAddress.toSshdSocketAddress(remoteAddress);
+ String host = address.getHostName().toLowerCase(Locale.ROOT);
+ return address.getPort() == DEFAULT_PORT ? host : "[" + host + "]:" +
address.getPort();
}
- private List<String> getKnownHostsFileTokensForSocketAddress(SocketAddress
remoteAddress) {
- List<String> returnList = new LinkedList<>();
- if (remoteAddress instanceof InetSocketAddress) {
- InetSocketAddress inetSocketAddress = (InetSocketAddress)
remoteAddress;
-
- String hostName = inetSocketAddress.getHostName();
- String ipAddress = inetSocketAddress.getAddress().getHostAddress();
- String remotePort = String.valueOf(inetSocketAddress.getPort());
-
- returnList.add(hostName);
- returnList.add("[" + hostName + "]:" + remotePort);
- returnList.add(ipAddress);
- returnList.add("[" + ipAddress + "]:" + remotePort);
+ /*
+ * Matches a pattern where * stands for any sequence of characters and ?
for any single character.
+ */
+ static boolean matchesPattern(String pattern, String name) {
+ int p = 0;
+ int n = 0;
+ int star = -1;
+ int resume = 0;
+ while (n < name.length()) {
+ if (p < pattern.length() && (pattern.charAt(p) == '?' ||
pattern.charAt(p) == name.charAt(n))) {
+ p++;
+ n++;
+ } else if (p < pattern.length() && pattern.charAt(p) == '*') {
+ star = p++;
+ resume = n;
+ } else if (star >= 0) {
+ p = star + 1;
+ n = ++resume;
+ } else {
+ return false;
+ }
}
-
- return returnList;
+ while (p < pattern.length() && pattern.charAt(p) == '*') {
+ p++;
+ }
+ return p == pattern.length();
}
/*
- * Decode the public key string, which is a base64 encoded string that
consists
- * of multiple parts: 1. public key type (ssh-rsa, ssh-dss, ...) 2. binary
key
- * data (May consists of multiple parts)
- *
- * Each part is composed by two sub-parts 1. Length of the part (4 bytes)
2.
- * Binary part (length as defined by 1.)
- *
- * Uses SSHPublicKeyHolder to construct the actual PublicKey Object
- *
- * Note: Currently only supports RSA and DSA Public keys as required by
- * https://tools.ietf.org/html/rfc4253#section-6.6
- *
+ * A line of the known_hosts file: an optional marker, the host patterns
or a hashed host, and the key. Anything
+ * after the key is a comment.
*/
- PublicKey loadKey(String key) throws NoSuchAlgorithmException,
InvalidKeySpecException {
- SSHPublicKeyHolder sshPublicKeyHolder = new SSHPublicKeyHolder();
-
- byte[] keyByteArray = Base64.getDecoder().decode(key);
- int keyByteArrayCursor = 0;
-
- byte[] tmpData = new byte[4];
- int tmpCursor = 0;
-
- boolean getLengthMode = true;
- while (keyByteArrayCursor < keyByteArray.length) {
- if (getLengthMode) {
- if (tmpCursor < 4) {
- tmpData[tmpCursor] = keyByteArray[keyByteArrayCursor];
- tmpCursor++;
- keyByteArrayCursor++;
- continue;
- } else {
- tmpCursor = 0;
- getLengthMode = false;
- tmpData = new byte[byteArrayToInt(tmpData)];
+ private static final class KnownHostsEntry {
+ private final String marker;
+ private final String hosts;
+ private final byte[] salt;
+ private final byte[] hash;
+ private final String keyType;
+ private final PublicKey key;
+
+ private KnownHostsEntry(String marker, String hosts, byte[] salt,
byte[] hash, String keyType, PublicKey key) {
+ this.marker = marker;
+ this.hosts = hosts;
+ this.salt = salt;
+ this.hash = hash;
+ this.keyType = keyType;
+ this.key = key;
+ }
+
+ /*
+ * Returns null when the line cannot be read. A key of a type that is
not supported is kept as a null key.
+ */
+ static KnownHostsEntry parse(String line, SessionContext session) {
+ String[] fields = line.split("\\s+");
+ int index = 0;
+ String marker = null;
+ if (fields[0].startsWith("@")) {
+ marker = fields[0];
+ if (!MARKER_REVOKED.equals(marker) &&
!MARKER_CERT_AUTHORITY.equals(marker)) {
+ return null;
}
+ index = 1;
}
- tmpData[tmpCursor] = keyByteArray[keyByteArrayCursor];
- tmpCursor++;
- keyByteArrayCursor++;
- if (tmpCursor == tmpData.length) {
- sshPublicKeyHolder.push(tmpData);
- getLengthMode = true;
- tmpData = new byte[4];
- tmpCursor = 0;
+ if (fields.length < index + 3) {
+ return null;
+ }
+ String hosts = fields[index];
+ try {
+ byte[] salt = null;
+ byte[] hash = null;
+ if (hosts.startsWith(HASHED_HOST_PREFIX)) {
+ String[] parts =
hosts.substring(HASHED_HOST_PREFIX.length()).split("\\|", -1);
+ if (parts.length != 2) {
+ return null;
+ }
+ salt = Base64.getDecoder().decode(parts[0]);
+ hash = Base64.getDecoder().decode(parts[1]);
+ if (salt.length != HASH_SALT_LENGTH) {
Review Comment:
Fixed in a1d570a52eef. A host field that starts with `|` must now be a
`|1|salt|hash` entry whose salt and hash both decode to 20 bytes. Anything else
makes the line unreadable: an empty, short or long digest, a short salt, or
another prefix such as `|2|`. An unreadable line makes every connection fail.
`rejectsEveryServerWhenALineCannotBeRead` covers each of these cases alone
and after a valid entry, with both values of `failOnUnknownHost`.
_Claude Code on behalf of Andrea Cosentino (@oscerd)_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]