luigidemasi commented on code in PR #27821:
URL: https://github.com/apache/camel/pull/27821#discussion_r4270491447


##########
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:
   **Major / P1: Preserve the configured IPv6 spelling for known-host lookup 
(pre-existing issue).**
   
   `SshdSocketAddress.toSshdSocketAddress(remoteAddress)` delegates to 
`InetSocketAddress.getHostString()`, which does not necessarily retain a 
numeric IPv6 literal as configured. Connecting to `::1` gives this verifier 
`0:0:0:0:0:0:0:1`.
   
   With a valid known_hosts entry for `[::1]:<port>`, the code instead looks 
for `[0:0:0:0:0:0:0:1]:<port>`. The configured server is consequently 
classified as unknown:
   - with `failOnUnknownHost=true`, even the correct listed key is rejected;
   - with the default `false`, an arbitrary different key is accepted.
   
   I reproduced both cases using a real IPv6 loopback SSH server. In the second 
case the server received the synthetic password. `ssh-keygen -F '[::1]:<port>'` 
recognizes the same fixture.
   
   This behavior also occurs before this PR; I am flagging an existing gap in 
the lookup path being changed/documented, not claiming a new regression. Please 
carry the configured host spelling through to verification rather than relying 
on the resolved socket address to preserve it, and add connection-level IPv6 
tests for the legitimate and mismatching keys with both values of 
`failOnUnknownHost`.
   
   _AI-generated review by Codex on behalf of @luigidemasi._



##########
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:
   **Required / P2: Validate the hashed-host digest as well as the salt.**
   
   Only the salt length is checked. A `|1|` entry with a valid 20-byte salt but 
an empty or truncated digest is accepted by the parser. It then never matches a 
host, so `failOnUnknownHost=false` permits an arbitrary key instead of 
rejecting the malformed resource.
   
   I reproduced this with a real loopback SSH connection: a valid hashed entry 
rejected the different server key with no password delivered; truncating its 
decoded digest from 20 bytes to 19 made the same different key authenticate and 
receive the synthetic password. Verifier probes also reproduce acceptance with 
an empty digest. A malformed hashed line after a valid trusted entry is ignored 
even in strict mode, contradicting the new promise that an unreadable line 
rejects every connection.
   
   Additionally, a host field beginning with an unsupported hash prefix such as 
`|2|` currently falls through to ordinary glob matching, with the same 
unknown-host fallback.
   
   Please require exactly 20 decoded bytes for the HMAC-SHA1 digest, and treat 
a leading `|` that does not form a supported, valid hashed entry as a parse 
failure. Extend `rejectsEveryServerWhenALineCannotBeRead` with valid-length 
salt plus empty/short/long digest and unsupported-prefix cases, both alone and 
after a valid entry, with both values of `failOnUnknownHost`.
   
   This requires malformed local configuration; it is a gap in the fail-closed 
guarantee, not a claim that an external peer can modify known_hosts.
   
   _AI-generated review by Codex on behalf of @luigidemasi._



-- 
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]

Reply via email to