This is an automated email from the ASF dual-hosted git repository.

twolf pushed a commit to branch dev_3.0
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit e7baf5dca57c45c80e14b9492a1e6b1e22cbda9c
Author: Thomas Wolf <tw...@apache.org>
AuthorDate: Fri Apr 25 23:59:29 2025 +0200

    Finish the "hostkeys...@openssh.com" host key rotation implementation
    
    Implement the client side as far as registering the host keys in the
    client session: send the signature challenge, and handle the reply.
    Once the keys are verified, invoke a new NewHostKeysHandler on the
    SshClient. (It gets the session as parameter so that it can figure out
    which server sent the keys. But the handler is installed on the client
    because if it updates the known_hosts file, that could affect many
    sessions.)
    
    Take care to handle certificates and RSA keys. Fix the server side to
    correctly deal with these, too.
    
    Updating the known_hosts file is not done; user code can install its
    own NewHostKeysHandler on the SshClient to do so.
    
    Remove the nested OpenSshCertificate.CertificateOption class; these
    options are better handled as Map<String, String>. Remove unnecessary
    generics in arguments: replace Collection<? extends PublicKey> by
    Collection<PublicKey>.
---
 CHANGES.md                                         |   1 +
 .../apache/sshd/common/config/keys/KeyUtils.java   |  13 +-
 .../common/config/keys/OpenSshCertificate.java     | 119 ++++------------
 .../common/config/keys/OpenSshCertificateImpl.java |  60 +-------
 .../org/apache/sshd/common/util/GenericUtils.java  |   2 +-
 .../org/apache/sshd/common/util/buffer/Buffer.java |  30 ++--
 .../certificate/OpenSshCertificateBuilder.java     |  28 ----
 .../java/org/apache/sshd/client/ClientBuilder.java |  13 ++
 .../apache/sshd/client/ClientFactoryManager.java   |   5 +
 .../java/org/apache/sshd/client/SshClient.java     |  12 ++
 .../client/config/DefaultNewHostKeysHandler.java   |  42 ++++++
 .../sshd/client/config/NewHostKeysHandler.java     |  50 +++++++
 .../sshd/client/global/OpenSshHostKeysHandler.java | 155 ++++++++++++++++++++-
 .../java/org/apache/sshd/client/kex/DHGClient.java |  16 +--
 .../sshd/client/session/AbstractClientSession.java |  83 ++++++-----
 .../apache/sshd/client/session/ClientSession.java  |  40 +++++-
 .../global/AbstractOpenSshHostKeysHandler.java     |   2 +-
 .../sshd/server/auth/pubkey/UserAuthPublicKey.java |  20 +--
 .../sshd/server/global/OpenSshHostKeysHandler.java |  27 +++-
 .../GenerateOpenSSHClientCertificateTest.java      |  25 ++--
 ...GenerateOpenSshClientCertificateOracleTest.java |  21 +--
 .../certificates/OpenSSHCertificateParserTest.java |   9 +-
 .../auth/pubkey/HostBoundPubKeyAuthTest.java       |  29 +++-
 .../common/global/OpenSshHostKeysHandlerTest.java  |   4 +-
 .../sshd/common/session/GlobalRequestTest.java     |   8 +-
 25 files changed, 498 insertions(+), 316 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 62a37ab9b..0a1d485c5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -24,3 +24,4 @@ Version 3 includes all the features and bug fixes of version 
2, including the [l
 * New event callback `SessionListener.sessionStarting()`. See the [filter 
documentation](./docs/technical/filters.md). 
`SessionListener.sessionEstablished()` was removed; it was called from the 
constructor of `AbstractSession` at a time when the object was not yet fully 
initialized.
 * [GH-728](https://github.com/apache/mina-sshd/issues/728) New method 
`ClientSession.getHostConfigEntry()` to get the `HostConfigEntry` for the 
session.
 * [GH-729](https://github.com/apache/mina-sshd/issues/729) Support for 
client-side SOCKS5 or HTTP CONNECT proxies. See the 
[documentation](./docs/client-setup.md#proxies).
+* The [OpenSSH "hostkeys...@openssh.com" host key rotation 
extension](https://github.com/openssh/openssh-portable/blob/b5b405fee/PROTOCOL#L367)
 is now implemented client-side. New host keys so received are registered on 
the session but we don't update the known_hosts file. If you want that, 
implement your own `NewHostKeysHandler` and set it on the `SshClient`.
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
index 5de8d0b3c..046ed3b88 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
@@ -168,9 +168,6 @@ public final class KeyUtils {
 
     static {
 
-        // order matters here, when, for example, 
SkED25519PublicKeyEntryDecoder is used, it registers the PrivateKey
-        // type as java.security.PrivateKey, and all PrivateKey types are 
assignable to this, so it must be consulted last
-
         registerPublicKeyEntryDecoder(OpenSSHCertificateDecoder.INSTANCE);
         registerPublicKeyEntryDecoder(RSAPublicKeyDecoder.INSTANCE);
         registerPublicKeyEntryDecoder(DSSPublicKeyEntryDecoder.INSTANCE);
@@ -182,9 +179,6 @@ public final class KeyUtils {
             
registerPublicKeyEntryDecoder(SecurityUtils.getEDDSAPublicKeyEntryDecoder());
         }
 
-        // order matters, these must be last since they register their 
PrivateKey type as java.security.PrivateKey
-        // there is logical code which discovers a decoder type by instance 
assignability to this registered type
-
         if (SecurityUtils.isECCSupported()) {
             
registerPublicKeyEntryDecoder(SkECDSAPublicKeyEntryDecoder.INSTANCE);
         }
@@ -1182,6 +1176,13 @@ public final class KeyUtils {
         }
     }
 
+    public static String getSignatureAlgorithm(String chosenAlgorithm) {
+        synchronized (SIGNATURE_ALGORITHM_MAP) {
+            String mapped = SIGNATURE_ALGORITHM_MAP.get(chosenAlgorithm);
+            return mapped == null ? chosenAlgorithm : mapped;
+        }
+    }
+
     public static boolean isCertificateAlgorithm(String algorithm) {
         synchronized (SIGNATURE_ALGORITHM_MAP) {
             return SIGNATURE_ALGORITHM_MAP.containsKey(algorithm);
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificate.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificate.java
index ab3e230a1..ef6a5899f 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificate.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificate.java
@@ -24,10 +24,11 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Objects;
 import java.util.SortedMap;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.signature.Signature;
 import org.apache.sshd.common.util.ValidateUtils;
 
 /**
@@ -144,35 +145,19 @@ public interface OpenSshCertificate extends SshPublicKey {
      */
     long getValidBefore();
 
-    /**
-     * Retrieves the critical options set in the certificate.
-     *
-     * @return the critical options as an unmodifiable list, never {@code 
null} but possibly empty
-     * @see    #getCriticalOptionsMap()
-     */
-    List<CertificateOption> getCriticalOptions();
-
     /**
      * Retrieves the critical options set in the certificate.
      *
      * @return the critical options as an unmodifiable map, never {@code null} 
but possibly empty
      */
-    SortedMap<String, String> getCriticalOptionsMap();
-
-    /**
-     * Retrieves the extensions set in the certificate.
-     *
-     * @return the extensions as an unmodifiable list, never {@code null} but 
possibly empty
-     * @see    #getExtensionsMap()
-     */
-    List<CertificateOption> getExtensions();
+    SortedMap<String, String> getCriticalOptions();
 
     /**
      * Retrieves the extensions set in the certificate.
      *
      * @return the extensions as an unmodifiable map, never {@code null} but 
possibly empty
      */
-    SortedMap<String, String> getExtensionsMap();
+    SortedMap<String, String> getExtensions();
 
     /**
      * Retrieves the "reserved" field of the certificate. OpenSSH currently 
doesn't use it and ignores it.
@@ -244,86 +229,32 @@ public interface OpenSshCertificate extends SshPublicKey {
     }
 
     /**
-     * Certificate Options are a set of bytes that is
+     * Verifies the signature of the certificate.
      *
-     * <pre>
-     * [overall length][name(string)][[length of buffer][[length of 
string][data(string)]]]...
-     * </pre>
-     * <p>
-     * Where each Certificate Option is encoded as a name (string) and data 
(string packed in a buffer). The entire name
-     * (string) + data (buffer) are added as bytes (which will get a length 
prefix).
-     * </p>
-     *
-     * @see <a href=
-     *      
"https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys#L221-L319";>PROTOCOL.certkeys</a>
+     * @param  cert               {@link OpenSshCertificate} to verify
+     * @param  signatureFactories Available signature factories for the 
signature verification
+     * @return                    {@code true} if the signature verifies, 
{@code false} otherwise
+     * @throws Exception          if the signature verifier reports an error
      */
-    class CertificateOption {
-
-        private final String name;
-        private final String data;
-
-        /**
-         * Creates a new {@link CertificateOption} with the given name and 
data.
-         *
-         * @param name of the option; must be neither {@code null} nor empty
-         * @param data for the option; may be {@code null} or empty
-         */
-        public CertificateOption(String name, String data) {
-            this.name = ValidateUtils.checkNotNullAndNotEmpty(name, 
"CertificateOption name must be set");
-            this.data = data;
-        }
-
-        /**
-         * Creates a new {@link CertificateOption} with a name without data.
-         *
-         * @param name of the option; must be neither {@code null} nor empty
-         */
-        public CertificateOption(String name) {
-            this(name, null);
-        }
-
-        /**
-         * Retrieves the name.
-         *
-         * @return the name, never {@code null}
-         */
-        public final String getName() {
-            return name;
-        }
-
-        /**
-         * Retrieves the data.
-         *
-         * @return the data, may be{@code null} or empty
-         */
-        public final String getData() {
-            return data;
+    static boolean verifySignature(OpenSshCertificate cert, 
List<NamedFactory<Signature>> signatureFactories)
+            throws Exception {
+        PublicKey signatureKey = cert.getCaPubKey();
+        if (signatureKey instanceof OpenSshCertificate || cert.getCertPubKey() 
instanceof OpenSshCertificate) {
+            // OpenSSH certificates do not allow certificate chains.
+            return false;
         }
-
-        @Override
-        public String toString() {
-            return "CertificateOption{name='" + name + "', data='" + data + 
"'}";
+        String keyAlg = KeyUtils.getKeyType(signatureKey);
+        String sigAlg = cert.getSignatureAlgorithm();
+        if (!keyAlg.equals(KeyUtils.getCanonicalKeyType(sigAlg))) {
+            return false;
         }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (obj == null) {
-                return false;
-            }
-            if (obj == this) {
-                return true;
-            }
-            if (getClass() != obj.getClass()) {
-                return false;
-            }
-
-            CertificateOption other = (CertificateOption) obj;
-            return Objects.equals(name, other.name) && Objects.equals(data, 
other.data);
+        Signature verifier = NamedFactory.create(signatureFactories, sigAlg);
+        if (verifier == null) {
+            return false;
         }
+        verifier.initVerifier(null, signatureKey);
+        verifier.update(null, cert.getMessage());
 
-        @Override
-        public int hashCode() {
-            return Objects.hash(name, data);
-        }
+        return verifier.verify(null, cert.getSignature());
     }
 }
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificateImpl.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificateImpl.java
index f2c182b54..b8e126520 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificateImpl.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/OpenSshCertificateImpl.java
@@ -20,11 +20,9 @@ package org.apache.sshd.common.config.keys;
 
 import java.security.PublicKey;
 import java.time.Instant;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
-import java.util.List;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.TreeMap;
@@ -116,32 +114,12 @@ public class OpenSshCertificateImpl implements 
OpenSshCertificate {
     }
 
     @Override
-    public List<CertificateOption> getCriticalOptions() {
-        if (criticalOptions == null || criticalOptions.isEmpty()) {
-            return Collections.emptyList();
-        }
-        List<CertificateOption> list = new ArrayList<>(criticalOptions.size());
-        criticalOptions.forEach((k, v) -> list.add(new CertificateOption(k, 
v)));
-        return Collections.unmodifiableList(list);
-    }
-
-    @Override
-    public SortedMap<String, String> getCriticalOptionsMap() {
+    public SortedMap<String, String> getCriticalOptions() {
         return criticalOptions == null ? Collections.emptySortedMap() : 
Collections.unmodifiableSortedMap(criticalOptions);
     }
 
     @Override
-    public List<CertificateOption> getExtensions() {
-        if (extensions == null || extensions.isEmpty()) {
-            return Collections.emptyList();
-        }
-        List<CertificateOption> list = new ArrayList<>(extensions.size());
-        extensions.forEach((k, v) -> list.add(new CertificateOption(k, v)));
-        return Collections.unmodifiableList(list);
-    }
-
-    @Override
-    public SortedMap<String, String> getExtensionsMap() {
+    public SortedMap<String, String> getExtensions() {
         return extensions == null ? Collections.emptySortedMap() : 
Collections.unmodifiableSortedMap(extensions);
     }
 
@@ -261,23 +239,6 @@ public class OpenSshCertificateImpl implements 
OpenSshCertificate {
         }
     }
 
-    /**
-     * Sets the critical options of the certificate, overriding any options 
set earlier.
-     *
-     * @param criticalOptions to set; may be {@code null} or empty to remove 
all previously set options
-     */
-    public void setCriticalOptions(List<CertificateOption> criticalOptions) {
-        if (criticalOptions == null || criticalOptions.isEmpty()) {
-            this.criticalOptions = null;
-        } else {
-            this.criticalOptions = new TreeMap<>();
-            for (CertificateOption option : criticalOptions) {
-                String data = option.getData();
-                this.criticalOptions.put(option.getName(), data == null ? "" : 
data);
-            }
-        }
-    }
-
     /**
      * Sets the critical options of the certificate, overriding any options 
set earlier.
      *
@@ -317,23 +278,6 @@ public class OpenSshCertificateImpl implements 
OpenSshCertificate {
         return criticalOptions.put(key, value) == null;
     }
 
-    /**
-     * Sets the extensions of the certificate, overriding any extensions set 
earlier.
-     *
-     * @param extensions to set; may be {@code null} or empty to remove all 
previously set extensions
-     */
-    public void setExtensions(List<CertificateOption> extensions) {
-        if (extensions == null || extensions.isEmpty()) {
-            this.extensions = null;
-        } else {
-            this.extensions = new TreeMap<>();
-            for (CertificateOption option : extensions) {
-                String data = option.getData();
-                this.extensions.put(option.getName(), data == null ? "" : 
data);
-            }
-        }
-    }
-
     /**
      * Sets the extensions of the certificate, overriding any extensions set 
earlier.
      *
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java 
b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java
index 57ffec51a..d2fe8e91a 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java
@@ -355,7 +355,7 @@ public final class GenericUtils {
     }
 
     public static boolean isEmpty(Collection<?> c) {
-        return size(c) <= 0;
+        return c == null || c.isEmpty();
     }
 
     public static boolean isNotEmpty(Collection<?> c) {
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java 
b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
index bb7bfc2d8..ee857d6b4 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
@@ -49,8 +49,10 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.function.IntUnaryOperator;
 import java.util.logging.Level;
@@ -371,7 +373,7 @@ public abstract class Buffer implements Readable {
         return GenericUtils.isEmpty(values) ? Collections.emptyList() : 
Arrays.asList(values);
     }
 
-    public List<OpenSshCertificate.CertificateOption> getCertificateOptions() {
+    public Map<String, String> getCertificateOptions() {
         return getCertificateOptions(StandardCharsets.UTF_8);
     }
 
@@ -390,8 +392,8 @@ public abstract class Buffer implements Readable {
      * @param  charset {@link Charset} to use for converting bytes to 
characters
      * @return         the parsed result, never {@code null}, but possibly 
empty
      */
-    public List<OpenSshCertificate.CertificateOption> 
getCertificateOptions(Charset charset) {
-        List<OpenSshCertificate.CertificateOption> list = new ArrayList<>();
+    public Map<String, String> getCertificateOptions(Charset charset) {
+        Map<String, String> result = new HashMap<>();
 
         if (available() > 0) {
             // pull out entire Certificate Options section
@@ -405,11 +407,11 @@ public abstract class Buffer implements Readable {
                     data = 
GenericUtils.trimToEmpty(dataBuffer.getString(charset));
                     data = data.length() > 0 ? data : null;
                 }
-                list.add(new OpenSshCertificate.CertificateOption(name, data));
+                result.put(name, data);
             }
         }
 
-        return list;
+        return result;
     }
 
     /**
@@ -841,7 +843,7 @@ public abstract class Buffer implements Readable {
         }
     }
 
-    public void 
putCertificateOptions(List<OpenSshCertificate.CertificateOption> options) {
+    public void putCertificateOptions(Map<String, String> options) {
         putCertificateOptions(options, StandardCharsets.UTF_8);
     }
 
@@ -860,26 +862,24 @@ public abstract class Buffer implements Readable {
      * @param options to write into the buffer, may be {@code null} or empty 
but must not contain {@code null} elements
      * @param charset The {@link Charset} to use for string options
      */
-    public void 
putCertificateOptions(List<OpenSshCertificate.CertificateOption> options, 
Charset charset) {
-        int numObjects = GenericUtils.size(options);
-
-        if (numObjects <= 0) {
+    public void putCertificateOptions(Map<String, String> options, Charset 
charset) {
+        if (options == null || options.size() == 0) {
             putBytes(GenericUtils.EMPTY_BYTE_ARRAY);
             return;
         }
 
         ByteArrayBuffer optionBuffer = new ByteArrayBuffer();
 
-        for (OpenSshCertificate.CertificateOption option : options) {
-            optionBuffer.putString(option.getName(), charset);
-            if (GenericUtils.isEmpty(option.getData())) {
+        options.forEach((key, value) -> {
+            optionBuffer.putString(key, charset);
+            if (GenericUtils.isEmpty(value)) {
                 optionBuffer.putBytes(GenericUtils.EMPTY_BYTE_ARRAY);
             } else {
                 ByteArrayBuffer dataBuffer = new ByteArrayBuffer();
-                dataBuffer.putString(option.getData(), charset);
+                dataBuffer.putString(value, charset);
                 optionBuffer.putBytes(dataBuffer.getCompactData());
             }
-        }
+        });
 
         putBytes(optionBuffer.getCompactData());
     }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/certificate/OpenSshCertificateBuilder.java
 
b/sshd-core/src/main/java/org/apache/sshd/certificate/OpenSshCertificateBuilder.java
index 5d5f51ed8..46c5977ed 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/certificate/OpenSshCertificateBuilder.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/certificate/OpenSshCertificateBuilder.java
@@ -26,14 +26,12 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 import org.apache.sshd.common.BaseBuilder;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.OpenSshCertificate;
-import org.apache.sshd.common.config.keys.OpenSshCertificate.CertificateOption;
 import org.apache.sshd.common.config.keys.OpenSshCertificate.Type;
 import org.apache.sshd.common.config.keys.OpenSshCertificateImpl;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
@@ -112,19 +110,6 @@ public class OpenSshCertificateBuilder {
         return this;
     }
 
-    public OpenSshCertificateBuilder 
criticalOptions(List<OpenSshCertificate.CertificateOption> criticalOptions) {
-        this.criticalOptions.clear();
-        for (CertificateOption option : criticalOptions) {
-            String key = 
ValidateUtils.checkNotNullAndNotEmpty(option.getName(), "Critical option name 
must be set");
-            String value = option.getData();
-            String prev = this.criticalOptions.put(key, value == null ? "" : 
value);
-            if (prev != null) {
-                throw new IllegalArgumentException("Duplicate certificate 
option " + key);
-            }
-        }
-        return this;
-    }
-
     public OpenSshCertificateBuilder criticalOptions(Map<String, String> 
criticalOptions) {
         this.criticalOptions.clear();
         criticalOptions.forEach((k, v) -> {
@@ -140,19 +125,6 @@ public class OpenSshCertificateBuilder {
         return this;
     }
 
-    public OpenSshCertificateBuilder 
extensions(List<OpenSshCertificate.CertificateOption> extensions) {
-        this.extensions.clear();
-        for (CertificateOption option : extensions) {
-            String key = 
ValidateUtils.checkNotNullAndNotEmpty(option.getName(), "Extension name must be 
set");
-            String value = option.getData();
-            String prev = this.extensions.put(key, value == null ? "" : value);
-            if (prev != null) {
-                throw new IllegalArgumentException("Duplicate certificate 
extension " + key);
-            }
-        }
-        return this;
-    }
-
     public OpenSshCertificateBuilder extensions(Map<String, String> 
extensions) {
         this.extensions.clear();
         extensions.forEach((k, v) -> {
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientBuilder.java 
b/sshd-core/src/main/java/org/apache/sshd/client/ClientBuilder.java
index 7876431ee..eec0bc53b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientBuilder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientBuilder.java
@@ -24,6 +24,8 @@ import java.util.Collections;
 import java.util.List;
 import java.util.function.Function;
 
+import org.apache.sshd.client.config.DefaultNewHostKeysHandler;
+import org.apache.sshd.client.config.NewHostKeysHandler;
 import org.apache.sshd.client.config.hosts.DefaultConfigFileHostEntryResolver;
 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
 import org.apache.sshd.client.config.keys.ClientIdentityLoader;
@@ -85,6 +87,7 @@ public class ClientBuilder extends BaseBuilder<SshClient, 
ClientBuilder> {
     protected ProxyDataFactory proxyDataFactory;
     protected ServerKeyVerifier serverKeyVerifier;
     protected HostConfigEntryResolver hostConfigEntryResolver;
+    protected NewHostKeysHandler newHostKeysHandler;
     protected ClientIdentityLoader clientIdentityLoader;
     protected FilePasswordProvider filePasswordProvider;
 
@@ -107,6 +110,11 @@ public class ClientBuilder extends BaseBuilder<SshClient, 
ClientBuilder> {
         return me();
     }
 
+    public ClientBuilder newHostKeysHandler(NewHostKeysHandler handler) {
+        this.newHostKeysHandler = handler;
+        return me();
+    }
+
     public ClientBuilder clientIdentityLoader(ClientIdentityLoader loader) {
         this.clientIdentityLoader = loader;
         return me();
@@ -157,6 +165,10 @@ public class ClientBuilder extends BaseBuilder<SshClient, 
ClientBuilder> {
             hostConfigEntryResolver = DEFAULT_HOST_CONFIG_ENTRY_RESOLVER;
         }
 
+        if (newHostKeysHandler == null) {
+            newHostKeysHandler = new DefaultNewHostKeysHandler();
+        }
+
         if (clientIdentityLoader == null) {
             clientIdentityLoader = DEFAULT_CLIENT_IDENTITY_LOADER;
         }
@@ -178,6 +190,7 @@ public class ClientBuilder extends BaseBuilder<SshClient, 
ClientBuilder> {
         client.setProxyDataFactory(proxyDataFactory);
         client.setServerKeyVerifier(serverKeyVerifier);
         client.setHostConfigEntryResolver(hostConfigEntryResolver);
+        client.setNewHostKeysHandler(newHostKeysHandler);
         client.setClientIdentityLoader(clientIdentityLoader);
         client.setFilePasswordProvider(filePasswordProvider);
         return client;
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java 
b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
index d16b7e592..bbee78c8a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sshd.client;
 
+import org.apache.sshd.client.config.NewHostKeysHandler;
 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
 import org.apache.sshd.client.config.keys.ClientIdentityLoaderManager;
 import org.apache.sshd.client.session.ClientSessionCreator;
@@ -44,4 +45,8 @@ public interface ClientFactoryManager
     HostConfigEntryResolver getHostConfigEntryResolver();
 
     void setHostConfigEntryResolver(HostConfigEntryResolver resolver);
+
+    NewHostKeysHandler getNewHostKeysHandler();
+
+    void setNewHostKeysHandler(NewHostKeysHandler handler);
 }
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java 
b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 5778bd426..22dad5edc 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -59,6 +59,7 @@ import 
org.apache.sshd.client.auth.password.PasswordIdentityProvider;
 import org.apache.sshd.client.auth.password.UserAuthPasswordFactory;
 import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
 import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
+import org.apache.sshd.client.config.NewHostKeysHandler;
 import org.apache.sshd.client.config.hosts.HostConfigEntry;
 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
 import org.apache.sshd.client.config.keys.ClientIdentity;
@@ -195,6 +196,7 @@ public class SshClient extends AbstractFactoryManager 
implements ClientFactoryMa
     private ProxyDataFactory proxyDataFactory;
     private ServerKeyVerifier serverKeyVerifier;
     private HostConfigEntryResolver hostConfigEntryResolver;
+    private NewHostKeysHandler newHostKeysHandler;
     private ClientIdentityLoader clientIdentityLoader;
     private KeyIdentityProvider keyIdentityProvider;
     private PublicKeyAuthenticationReporter publicKeyAuthenticationReporter;
@@ -248,6 +250,16 @@ public class SshClient extends AbstractFactoryManager 
implements ClientFactoryMa
         this.hostConfigEntryResolver = Objects.requireNonNull(resolver, "No 
host configuration entry resolver");
     }
 
+    @Override
+    public NewHostKeysHandler getNewHostKeysHandler() {
+        return newHostKeysHandler;
+    }
+
+    @Override
+    public void setNewHostKeysHandler(NewHostKeysHandler handler) {
+        newHostKeysHandler = Objects.requireNonNull(handler, "No handler for 
new host keys");
+    }
+
     @Override
     public FilePasswordProvider getFilePasswordProvider() {
         return filePasswordProvider;
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/config/DefaultNewHostKeysHandler.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/config/DefaultNewHostKeysHandler.java
new file mode 100644
index 000000000..f8751f5e8
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/config/DefaultNewHostKeysHandler.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.client.config;
+
+import java.security.PublicKey;
+import java.util.Collection;
+
+import org.apache.sshd.client.session.ClientSession;
+
+/**
+ * A default {@link NewHostKeysHandler} that registers the new host keys on 
the session.
+ *
+ * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
+ */
+public class DefaultNewHostKeysHandler implements NewHostKeysHandler {
+
+    public DefaultNewHostKeysHandler() {
+        super();
+    }
+
+    @Override
+    public void receiveNewHostKeys(ClientSession session, 
Collection<PublicKey> hostKeys) {
+        hostKeys.forEach(session::registerHostKey);
+    }
+
+}
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/config/NewHostKeysHandler.java 
b/sshd-core/src/main/java/org/apache/sshd/client/config/NewHostKeysHandler.java
new file mode 100644
index 000000000..ec5270581
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/config/NewHostKeysHandler.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.client.config;
+
+import java.security.PublicKey;
+import java.util.Collection;
+
+import org.apache.sshd.client.session.ClientSession;
+
+/**
+ * A handler that knows what to do when a {@link ClientSession} got new host 
keys from a server via the OpenSSH
+ * "hostkeys...@openssh.com" host key rotation extension.
+ *
+ * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
+ * @see    <a 
href="https://github.com/openssh/openssh-portable/blob/b5b405fee/PROTOCOL#L367";>OpenSSH
 host key
+ *         rotation</a>
+ */
+@FunctionalInterface
+public interface NewHostKeysHandler {
+
+    /**
+     * Invoked when new keys have been received and verified.
+     * <p>
+     * If this method updates the {@code known_hosts} file or other key 
database with the new keys, it is recommended to
+     * do so in a separate thread to avoid blocking the calling thread.
+     * </p>
+     *
+     * @param session  the {@link ClientSession} that received the keys; can 
be used to figure out which server sent
+     *                 these keys (via the session's host config entry or 
server address)
+     * @param hostKeys the verified host keys received; never {@code null} and 
never containing {@code null}; may
+     *                 contain host certificates
+     */
+    void receiveNewHostKeys(ClientSession session, Collection<PublicKey> 
hostKeys);
+}
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/global/OpenSshHostKeysHandler.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/global/OpenSshHostKeysHandler.java
index 817fdf7c3..c9db3c9d7 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/global/OpenSshHostKeysHandler.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/global/OpenSshHostKeysHandler.java
@@ -19,20 +19,35 @@
 
 package org.apache.sshd.client.global;
 
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
 import java.security.PublicKey;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
 
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.UnsupportedSshPublicKey;
 import org.apache.sshd.common.global.AbstractOpenSshHostKeysHandler;
+import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.signature.Signature;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.BufferException;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.buffer.keys.BufferPublicKeyParser;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
- * A handler for the &quot;hostkeys...@openssh.com&quot; request - for now, 
only reads the presented host key. One can
- * override the {@link #handleHostKeys(Session, Collection, boolean, Buffer)} 
methods in order to do something with the
- * keys
+ * A handler for the "hostkeys...@openssh.com" request.
  *
  * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
  * @see    <a 
href="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL";>OpenSSH 
protocol - section 2.5</a>
@@ -52,8 +67,7 @@ public class OpenSshHostKeysHandler extends 
AbstractOpenSshHostKeysHandler {
     }
 
     @Override
-    protected Result handleHostKeys(
-            Session session, Collection<? extends PublicKey> keys, boolean 
wantReply, Buffer buffer)
+    protected Result handleHostKeys(Session session, Collection<PublicKey> 
keys, boolean wantReply, Buffer buffer)
             throws Exception {
         // according to the spec, no reply should be required
         ValidateUtils.checkTrue(!wantReply, "Unexpected reply required for the 
host keys of %s", session);
@@ -61,7 +75,136 @@ public class OpenSshHostKeysHandler extends 
AbstractOpenSshHostKeysHandler {
             log.debug("handleHostKeys({})[want-reply={}] received {} keys",
                     session, wantReply, GenericUtils.size(keys));
         }
-
+        // Build and send the signature request, then verify signatures.
+        ClientSession client = ValidateUtils.checkInstanceOf(session, 
ClientSession.class,
+                "handleHostKeys(%s) called on a ServerSession", session);
+        List<PublicKey> validKeys = keys.stream().filter(key -> {
+            if (key instanceof OpenSshCertificate) {
+                return isValidHostCertificate(client, (OpenSshCertificate) 
key);
+            }
+            return !(key instanceof UnsupportedSshPublicKey);
+        }).collect(Collectors.toList());
+        Buffer request = 
session.createBuffer(SshConstants.SSH_MSG_GLOBAL_REQUEST);
+        
request.putString(org.apache.sshd.server.global.OpenSshHostKeysHandler.REQUEST);
+        request.putBoolean(true); // want-reply
+        validKeys.forEach(request::putPublicKey);
+        client.request(request, 
org.apache.sshd.server.global.OpenSshHostKeysHandler.REQUEST,
+                (cmd, reply) -> {
+                    if (cmd == SshConstants.SSH_MSG_REQUEST_SUCCESS) {
+                        handleHostKeyRotation(client, validKeys, reply);
+                    }
+                });
         return Result.Replied;
     }
+
+    protected void handleHostKeyRotation(ClientSession client, List<PublicKey> 
proposedKeys, Buffer reply) {
+        List<PublicKey> newKeys = new ArrayList<>();
+        proposedKeys.forEach(k -> {
+            byte[] signature = reply.getBytes();
+            String keyType = KeyUtils.getKeyType(k);
+            PublicKey signingKey = k;
+            if (k instanceof OpenSshCertificate) {
+                signingKey = ((OpenSshCertificate) k).getCertPubKey();
+            }
+            String algo = KeyUtils.getKeyType(signingKey);
+            // RSA is special...
+            if (KeyPairProvider.SSH_RSA.equals(algo)) {
+                // If a RSA host key was negotiated in KEX, use the signature 
algorithm from there:
+                String negotiated = 
client.getKexNegotiationResult().get(KexProposalOption.ALGORITHMS);
+                String canonical = KeyUtils.getCanonicalKeyType(negotiated);
+                if (KeyPairProvider.SSH_RSA.equals(canonical) || 
KeyPairProvider.SSH_RSA_CERT.equals(canonical)) {
+                    algo = KeyUtils.getSignatureAlgorithm(negotiated);
+                } else {
+                    // Look at what kind of signature we have. We accept only 
RSA_SHA256/512
+                    Buffer sigBuf = new ByteArrayBuffer(signature);
+                    try {
+                        algo = sigBuf.getString();
+                        if (KeyPairProvider.SSH_RSA.equals(algo)
+                                || 
!KeyPairProvider.SSH_RSA.equals(KeyUtils.getCanonicalKeyType(algo))) {
+                            return;
+                        }
+                    } catch (BufferException e) {
+                        if (log.isDebugEnabled()) {
+                            log.debug("handleHostKeyRotation({}) ignoring {} 
key because signature data is invalid", client,
+                                    keyType);
+                        }
+                        return;
+                    }
+                }
+            }
+            // Verify the signature.
+            Signature verifier = 
NamedFactory.create(client.getSignatureFactories(), algo);
+            if (verifier == null) {
+                if (log.isDebugEnabled()) {
+                    log.debug("handleHostKeyRotation({}) ignoring {} key 
because no signature verifier for {}", client, keyType,
+                            algo);
+                }
+                return;
+            }
+            Buffer expected = new ByteArrayBuffer();
+            
expected.putString(org.apache.sshd.server.global.OpenSshHostKeysHandler.REQUEST);
+            expected.putBytes(client.getSessionId());
+            expected.putPublicKey(k);
+            byte[] data = expected.getCompactData();
+            try {
+                verifier.initVerifier(client, signingKey);
+                verifier.update(client, data);
+                if (!verifier.verify(client, signature)) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("handleHostKeyRotation({}) ignoring {} key 
because {} signature doesn't match", client,
+                                keyType, algo);
+                    }
+                    return;
+                }
+            } catch (Exception e) {
+                if (log.isDebugEnabled()) {
+                    log.debug("handleHostKeyRotation({}) ignoring {} key: 
exception during {} signature verification: {}",
+                            client, keyType, algo, e.toString());
+                }
+                return;
+            }
+            newKeys.add(k);
+        });
+        if (reply.available() > 0) {
+            log.warn("handleHostKeyRotation({}) extra data of {} bytes 
ignored", client, reply.available());
+        }
+        if (!newKeys.isEmpty()) {
+            
client.getFactoryManager().getNewHostKeysHandler().receiveNewHostKeys(client, 
newKeys);
+        }
+    }
+
+    private boolean isValidHostCertificate(ClientSession session, 
OpenSshCertificate cert) {
+        if (!OpenSshCertificate.Type.HOST.equals(cert.getType()) || 
!OpenSshCertificate.isValidNow(cert)
+                || !cert.getCriticalOptions().isEmpty()) {
+            return false;
+        }
+        try {
+            if (!OpenSshCertificate.verifySignature(cert, 
session.getSignatureFactories())) {
+                return false;
+            }
+        } catch (Exception e) {
+            if (log.isDebugEnabled()) {
+                log.debug("isValidHostCertificate({}) could not verify host 
ertificate signature; got {}", session,
+                        e.toString());
+            }
+            return false;
+        }
+        // Empty principals in a host certificate mean the certificate is 
valid for any host.
+        Collection<String> principals = cert.getPrincipals();
+        if (!GenericUtils.isEmpty(principals)) {
+            /*
+             * We compare only the connect address against the principals and 
do not do any reverse DNS lookups. If one
+             * wants to connect with the IP it has to be included in the 
principals list of the certificate.
+             */
+            SocketAddress connectSocketAddress = session.getConnectAddress();
+            String hostName = null;
+            if (connectSocketAddress instanceof SshdSocketAddress) {
+                hostName = ((SshdSocketAddress) 
connectSocketAddress).getHostName();
+            } else if (connectSocketAddress instanceof InetSocketAddress) {
+                hostName = ((InetSocketAddress) 
connectSocketAddress).getHostString();
+            }
+            return hostName != null && principals.contains(hostName);
+        }
+        return true;
+    }
 }
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java 
b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java
index bab1928f0..cc9bdbe7d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java
@@ -252,19 +252,7 @@ public class DHGClient extends AbstractDHClientKeyExchange 
{
                     "KeyExchange signature verification failed, CA expired for 
key ID=" + keyId);
         }
 
-        String sigAlg = openSshKey.getSignatureAlgorithm();
-        if (!keyAlg.equals(KeyUtils.getCanonicalKeyType(sigAlg))) {
-            throw new 
SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
-                    "Found invalid signature alg " + sigAlg + " for key ID=" + 
keyId + " using a " + keyAlg + " CA key");
-        }
-
-        Signature verif = ValidateUtils.checkNotNull(
-                NamedFactory.create(session.getSignatureFactories(), sigAlg),
-                "No KeyExchange CA verifier located for algorithm=%s of key 
ID=%s", sigAlg, keyId);
-        verif.initVerifier(session, signatureKey);
-        verif.update(session, openSshKey.getMessage());
-
-        if (!verif.verify(session, openSshKey.getSignature())) {
+        if (!OpenSshCertificate.verifySignature(openSshKey, 
session.getSignatureFactories())) {
             throw new 
SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
                     "KeyExchange CA signature verification failed for key 
type=" + keyAlg + " of key ID=" + keyId);
         }
@@ -298,7 +286,7 @@ public class DHGClient extends AbstractDHClientKeyExchange {
             }
         }
 
-        if (!GenericUtils.isEmpty(openSshKey.getCriticalOptions())) {
+        if (!openSshKey.getCriticalOptions().isEmpty()) {
             // no critical option defined for host keys yet
             throw new 
SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
                     "KeyExchange signature verification failed, unrecognized 
critical options "
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index 38225cd12..8ff043a81 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -25,11 +25,13 @@ import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.security.KeyPair;
 import java.security.PublicKey;
-import java.util.HashSet;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.apache.sshd.client.ClientFactoryManager;
@@ -95,7 +97,7 @@ public abstract class AbstractClientSession extends 
AbstractSession implements C
     // Keep a record of already known and accepted host keys for this session. 
This enables us to bypass the
     // ServerKeyVerifier if a re-KEX returned the same key, or even a 
different key it had announced before via the
     // OpenSSH "hostkeys...@openssh.com" SSH_MSG_GLOBAL_REQUEST.
-    private final Set<String> sessionHostKeys = new HashSet<>();
+    private final Set<String> sessionHostKeys = ConcurrentHashMap.newKeySet();
     private final List<Object> identities = new CopyOnWriteArrayList<>();
     private final AuthenticationIdentitiesProvider identitiesProvider;
     private final AttributeRepository connectionContext;
@@ -177,6 +179,53 @@ public abstract class AbstractClientSession extends 
AbstractSession implements C
         this.serverKey = serverKey;
     }
 
+    @Override
+    public void registerHostKey(PublicKey hostKey) {
+        if (hostKey instanceof OpenSshCertificate) {
+            OpenSshCertificate cert = (OpenSshCertificate) hostKey;
+            
ValidateUtils.checkTrue(OpenSshCertificate.Type.HOST.equals(cert.getType()), 
"Invalid certificate type");
+            sessionHostKeys.add("@cert-authority " + 
PublicKeyEntry.toString(cert.getCaPubKey()));
+        } else {
+            ValidateUtils.checkNotNull(hostKey, "Null host key cannot be 
registered");
+            sessionHostKeys.add(PublicKeyEntry.toString(hostKey));
+        }
+    }
+
+    @Override
+    public Collection<String> getRegisteredHostKeys() {
+        return Collections.unmodifiableSet(sessionHostKeys);
+    }
+
+    @Override
+    protected void checkKeys() throws IOException {
+        PublicKey hostKey = Objects.requireNonNull(getServerKey(), "No server 
key to verify");
+        String serializedKey;
+        if (hostKey instanceof OpenSshCertificate) {
+            // Expiration etc. has already been checked.
+            serializedKey = "@cert-authority " + 
PublicKeyEntry.toString(((OpenSshCertificate) hostKey).getCaPubKey());
+        } else {
+            serializedKey = PublicKeyEntry.toString(hostKey);
+        }
+        if (!sessionHostKeys.contains(serializedKey)) {
+            ServerKeyVerifier verifier = 
Objects.requireNonNull(getServerKeyVerifier(), "No server key verifier");
+            IoSession networkSession = getIoSession();
+            SocketAddress remoteAddress = networkSession.getRemoteAddress();
+            SshdSocketAddress targetServerAddress = 
getAttribute(ClientSessionCreator.TARGET_SERVER);
+            if (targetServerAddress != null) {
+                remoteAddress = targetServerAddress.toInetSocketAddress();
+            }
+            boolean verified = verifier.verifyServerKey(this, remoteAddress, 
hostKey);
+            if (log.isDebugEnabled()) {
+                log.debug("checkKeys({}) key={}-{}, verified={}", this, 
KeyUtils.getKeyType(hostKey),
+                        KeyUtils.getFingerPrint(hostKey), verified);
+            }
+            if (!verified) {
+                throw new 
SshException(SshConstants.SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE, "Server key 
did not validate");
+            }
+            sessionHostKeys.add(serializedKey);
+        }
+    }
+
     @Override
     public ServerKeyVerifier getServerKeyVerifier() {
         ClientFactoryManager manager = getFactoryManager();
@@ -528,36 +577,6 @@ public abstract class AbstractClientSession extends 
AbstractSession implements C
         }
     }
 
-    @Override
-    protected void checkKeys() throws IOException {
-        PublicKey hostKey = Objects.requireNonNull(getServerKey(), "No server 
key to verify");
-        String serializedKey;
-        if (hostKey instanceof OpenSshCertificate) {
-            // Expiration etc. has already been checked.
-            serializedKey = "cert-authority " + 
PublicKeyEntry.toString(((OpenSshCertificate) hostKey).getCaPubKey());
-        } else {
-            serializedKey = PublicKeyEntry.toString(hostKey);
-        }
-        if (!sessionHostKeys.contains(serializedKey)) {
-            ServerKeyVerifier verifier = 
Objects.requireNonNull(getServerKeyVerifier(), "No server key verifier");
-            IoSession networkSession = getIoSession();
-            SocketAddress remoteAddress = networkSession.getRemoteAddress();
-            SshdSocketAddress targetServerAddress = 
getAttribute(ClientSessionCreator.TARGET_SERVER);
-            if (targetServerAddress != null) {
-                remoteAddress = targetServerAddress.toInetSocketAddress();
-            }
-            boolean verified = verifier.verifyServerKey(this, remoteAddress, 
hostKey);
-            if (log.isDebugEnabled()) {
-                log.debug("checkKeys({}) key={}-{}, verified={}", this, 
KeyUtils.getKeyType(hostKey),
-                        KeyUtils.getFingerPrint(hostKey), verified);
-            }
-            if (!verified) {
-                throw new 
SshException(SshConstants.SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE, "Server key 
did not validate");
-            }
-            sessionHostKeys.add(serializedKey);
-        }
-    }
-
     @Override
     public KeyExchangeFuture switchToNoneCipher() throws IOException {
         Service service = currentService.getService();
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java 
b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index d649309d8..7e2f027f8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -49,6 +49,7 @@ import org.apache.sshd.client.channel.ClientChannel;
 import org.apache.sshd.client.channel.ClientChannelEvent;
 import org.apache.sshd.client.config.hosts.HostConfigEntry;
 import org.apache.sshd.client.future.AuthFuture;
+import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.forward.DynamicPortForwardingTracker;
 import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
 import org.apache.sshd.common.AttributeRepository;
@@ -132,12 +133,47 @@ public interface ClientSession
     AuthFuture auth() throws IOException;
 
     /**
-     * Retrieves the server's key
+     * Retrieves the server's host key: the one it presented in the last key 
exchange.
      *
-     * @return The server's {@link PublicKey} - {@code null} if KEX not 
started or not completed
+     * @return the server's {@link PublicKey}; may be {@code null} if no key 
exchange has been completed yet
      */
     PublicKey getServerKey();
 
+    /**
+     * Explicitly registers a known host key for the session. Intended to be 
used if the server announces additional
+     * host keys as part of the OpenSSH "hostkeys...@openssh.com" extension, 
but it can also be used to register any
+     * host key at any other time.
+     * <p>
+     * If the server presents a host key that matches any of the ones 
registered in the session, the key will be
+     * accepted implicitly without consulting the {@link ServerKeyVerifier}.
+     * </p>
+     * <p>
+     * If the {@code hostKey} is a host certificate, the CA key will be 
registered. When the server presents a
+     * certificate, the certificate will be checked in any case and will be 
rejected if it is invalid or expired. If it
+     * is valid and not expired, it will be accepted implicitly if its CA key 
is already registered.
+     * </p>
+     *
+     * @param  hostKey                  the host key to register
+     * @throws IllegalArgumentException if {@code hostKey} is a user 
certificate
+     */
+    void registerHostKey(PublicKey hostKey);
+
+    /**
+     * Obtains a read-only collection of all currently registered server host 
keys is OpenSSH format, i.e. key type as a
+     * string, followed by the base64-encoded key, prefixed with 
"@cert-authority" if it's a CA key.
+     * <p>
+     * For a normal key: "key-type AAAA..."; for a host certificate CA key 
"@cert-authority key-type AAAA...".
+     * </p>
+     * <p>
+     * Note that this is not <em>quite</em> the format used in a OpenSSH 
{@code known_hosts} file as there are no host
+     * patterns.
+     * </p>
+     *
+     * @return all currently registered server host keys; never {@code null} 
but possibly empty, and never containing
+     *         {@code null}
+     */
+    Collection<String> getRegisteredHostKeys();
+
     /**
      * Create a channel of the given type. Same as calling 
<code>createChannel(type, null)</code>.
      *
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/global/AbstractOpenSshHostKeysHandler.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/global/AbstractOpenSshHostKeysHandler.java
index 60c25675d..f994ea87b 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/global/AbstractOpenSshHostKeysHandler.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/global/AbstractOpenSshHostKeysHandler.java
@@ -106,7 +106,7 @@ public abstract class AbstractOpenSshHostKeysHandler 
extends AbstractConnectionS
     }
 
     protected abstract Result handleHostKeys(
-            Session session, Collection<? extends PublicKey> keys, boolean 
wantReply, Buffer buffer)
+            Session session, Collection<PublicKey> keys, boolean wantReply, 
Buffer buffer)
             throws Exception;
 
     @Override
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java
index d16534373..2195f2de9 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java
@@ -182,28 +182,14 @@ public class UserAuthPublicKey extends AbstractUserAuth 
implements SignatureFact
     }
 
     protected void verifyCertificateSignature(ServerSession session, 
OpenSshCertificate cert) throws Exception {
-        PublicKey signatureKey = cert.getCaPubKey();
-        String keyAlg = KeyUtils.getKeyType(signatureKey);
-        String keyId = cert.getId();
-
-        String sigAlg = cert.getSignatureAlgorithm();
-        if (!keyAlg.equals(KeyUtils.getCanonicalKeyType(sigAlg))) {
+        if (!OpenSshCertificate.verifySignature(cert, 
session.getSignatureFactories())) {
             throw new CertificateException(
-                    "Found invalid signature alg " + sigAlg + " for key ID=" + 
keyId + " using a " + keyAlg + " CA key");
-        }
-
-        Signature verif = 
ValidateUtils.checkNotNull(NamedFactory.create(session.getSignatureFactories(), 
sigAlg),
-                "No CA verifier located for algorithm=%s of key ID=%s", 
sigAlg, keyId);
-        verif.initVerifier(session, signatureKey);
-        verif.update(session, cert.getMessage());
-
-        if (!verif.verify(session, cert.getSignature())) {
-            throw new CertificateException("CA signature verification failed 
for key type=" + keyAlg + " of key ID=" + keyId);
+                    "CA signature verification failed for key type=" + 
cert.getKeyType() + " of key ID=" + cert.getId());
         }
     }
 
     protected void verifyCertificateSources(ServerSession session, 
OpenSshCertificate cert) throws CertificateException {
-        String allowedSources = 
cert.getCriticalOptionsMap().get("source-address");
+        String allowedSources = 
cert.getCriticalOptions().get("source-address");
         if (allowedSources == null) {
             return;
         }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/global/OpenSshHostKeysHandler.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/global/OpenSshHostKeysHandler.java
index 628c6b883..d7d79bd20 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/global/OpenSshHostKeysHandler.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/global/OpenSshHostKeysHandler.java
@@ -30,7 +30,9 @@ import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.RuntimeSshException;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
 import org.apache.sshd.common.global.AbstractOpenSshHostKeysHandler;
+import org.apache.sshd.common.kex.KexProposalOption;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.signature.Signature;
@@ -86,7 +88,7 @@ public class OpenSshHostKeysHandler extends 
AbstractOpenSshHostKeysHandler imple
 
     @Override
     protected Result handleHostKeys(
-            Session session, Collection<? extends PublicKey> keys, boolean 
wantReply, Buffer buffer)
+            Session session, Collection<PublicKey> keys, boolean wantReply, 
Buffer buffer)
             throws Exception {
         // according to the specification there MUST be reply required by the 
server
         ValidateUtils.checkTrue(wantReply, "No reply required for host keys of 
%s", session);
@@ -107,11 +109,26 @@ public class OpenSshHostKeysHandler extends 
AbstractOpenSshHostKeysHandler imple
         KeyPairProvider kpp = Objects.requireNonNull(
                 ((ServerSession) session).getKeyPairProvider(), "No server 
keys provider");
         for (PublicKey k : keys) {
-            String keyType = KeyUtils.getKeyType(k);
+            PublicKey signingKey = k;
+            if (k instanceof OpenSshCertificate) {
+                signingKey = ((OpenSshCertificate) k).getCertPubKey();
+            }
+            String keyType = KeyUtils.getKeyType(signingKey);
+            String algo = keyType;
+            // RSA is special...
+            if (KeyPairProvider.SSH_RSA.equals(algo)) {
+                // If a RSA host key was negotiated in KEX, use the signature 
algorithm from there:
+                String negotiated = 
session.getKexNegotiationResult().get(KexProposalOption.ALGORITHMS);
+                String canonical = KeyUtils.getCanonicalKeyType(negotiated);
+                if (KeyPairProvider.SSH_RSA.equals(canonical) || 
KeyPairProvider.SSH_RSA_CERT.equals(canonical)) {
+                    algo = KeyUtils.getSignatureAlgorithm(negotiated);
+                } else {
+                    algo = KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS;
+                }
+            }
+
             Signature verifier = ValidateUtils.checkNotNull(
-                    NamedFactory.create(factories, keyType),
-                    "No signer could be located for key type=%s",
-                    keyType);
+                    NamedFactory.create(factories, algo), "No signer could be 
located for key type=%s, algo=%s", keyType, algo);
 
             KeyPair kp;
             try {
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSSHClientCertificateTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSSHClientCertificateTest.java
index fc8e58ba8..9f37cc06c 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSSHClientCertificateTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSSHClientCertificateTest.java
@@ -26,6 +26,8 @@ import java.security.KeyPair;
 import java.security.PublicKey;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.apache.sshd.certificate.OpenSshCertificateBuilder;
 import org.apache.sshd.common.BaseBuilder;
@@ -137,22 +139,23 @@ public class GenerateOpenSSHClientCertificateTest extends 
BaseTestSupport {
         if (i > 0) {
             signatureAlgorithm = "rsa-sha2-" + caName.substring(i + 5);
         }
+        Map<String, String> options = new HashMap<>();
+        options.put("force-command", "/path/to/script.sh");
+        options.put("source-address", "127.0.0.1/32");
+        options.put("verify-required", null);
+        Map<String, String> extensions = new HashMap<>();
+        extensions.put("no-touch-required", null);
+        extensions.put("permit-X11-forwarding", null);
+        extensions.put("permit-agent-forwarding", null);
+        extensions.put("permit-port-forwarding", null);
+        extensions.put("permit-pty", null);
+        extensions.put("permit-user-rc", null);
         final OpenSshCertificate signedCert = 
OpenSshCertificateBuilder.userCertificate()
                 .serial(0L)
                 .publicKey(clientPublicKey)
                 .id("user01")
                 .principals(Collections.singletonList("user01"))
-                .criticalOptions(Arrays.asList(
-                        new 
OpenSshCertificate.CertificateOption("force-command", "/path/to/script.sh"),
-                        new 
OpenSshCertificate.CertificateOption("source-address", "127.0.0.1/32"),
-                        new 
OpenSshCertificate.CertificateOption("verify-required")))
-                .extensions(Arrays.asList(
-                        new 
OpenSshCertificate.CertificateOption("no-touch-required"),
-                        new 
OpenSshCertificate.CertificateOption("permit-X11-forwarding"),
-                        new 
OpenSshCertificate.CertificateOption("permit-agent-forwarding"),
-                        new 
OpenSshCertificate.CertificateOption("permit-port-forwarding"),
-                        new OpenSshCertificate.CertificateOption("permit-pty"),
-                        new 
OpenSshCertificate.CertificateOption("permit-user-rc")))
+                .criticalOptions(options).extensions(extensions)
                 .sign(caKeypair, signatureAlgorithm);
 
         // encode to ssh public key format
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSshClientCertificateOracleTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSshClientCertificateOracleTest.java
index 71d2b5b7e..0cd105ac2 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSshClientCertificateOracleTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSshClientCertificateOracleTest.java
@@ -25,6 +25,8 @@ import java.security.KeyPair;
 import java.security.PublicKey;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.apache.sshd.certificate.OpenSshCertificateBuilder;
 import org.apache.sshd.common.BaseBuilder;
@@ -114,21 +116,22 @@ class GenerateOpenSshClientCertificateOracleTest extends 
BaseTestSupport {
 
         final KeyPair caKeypair = 
keyPairProvider.loadKeys(null).iterator().next();
 
+        Map<String, String> options = new HashMap<>();
+        options.put("source-address", "127.0.0.1/32");
+        options.put("force-command", "/path/to/script.sh");
+        Map<String, String> extensions = new HashMap<>();
+        extensions.put("permit-pty", null);
+        extensions.put("permit-agent-forwarding", null);
+        extensions.put("permit-user-rc", null);
+        extensions.put("permit-port-forwarding", null);
+        extensions.put("permit-X11-forwarding", null);
         final OpenSshCertificate signedCert = 
OpenSshCertificateBuilder.userCertificate()
                 .serial(0L)
                 .publicKey(clientPublicKey)
                 .id("user01")
                 .principals(Collections.singletonList("user01"))
                 .nonce(oracle.getNonce())
-                .criticalOptions(Arrays.asList(
-                        new 
OpenSshCertificate.CertificateOption("source-address", "127.0.0.1/32"),
-                        new 
OpenSshCertificate.CertificateOption("force-command", "/path/to/script.sh")))
-                .extensions(Arrays.asList(
-                        new OpenSshCertificate.CertificateOption("permit-pty"),
-                        new 
OpenSshCertificate.CertificateOption("permit-agent-forwarding"),
-                        new 
OpenSshCertificate.CertificateOption("permit-user-rc"),
-                        new 
OpenSshCertificate.CertificateOption("permit-port-forwarding"),
-                        new 
OpenSshCertificate.CertificateOption("permit-X11-forwarding")))
+                .criticalOptions(options).extensions(extensions)
                 .sign(caKeypair, params.algorithm);
 
         // Check that they both validate
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
index 988513339..149778562 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
@@ -87,13 +87,8 @@ class OpenSSHCertificateParserTest extends BaseTestSupport {
             assertEquals(OpenSshCertificate.INFINITY, 
typedCert.getValidBefore()); // forever
             assertTrue(typedCert.getCriticalOptions().isEmpty());
             assertEquals(
-                    Arrays.asList(
-                            new 
OpenSshCertificate.CertificateOption("permit-X11-forwarding", ""),
-                            new 
OpenSshCertificate.CertificateOption("permit-agent-forwarding", ""),
-                            new 
OpenSshCertificate.CertificateOption("permit-port-forwarding", ""),
-                            new 
OpenSshCertificate.CertificateOption("permit-pty", ""),
-                            new 
OpenSshCertificate.CertificateOption("permit-user-rc", "")),
-                    typedCert.getExtensions());
+                    "{permit-X11-forwarding=, permit-agent-forwarding=, 
permit-port-forwarding=, permit-pty=, permit-user-rc=}",
+                    typedCert.getExtensions().toString());
             assertEquals(params.sigAlgorithm, 
typedCert.getSignatureAlgorithm());
             verifySignature(typedCert);
             Buffer buffer = new ByteArrayBuffer();
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/HostBoundPubKeyAuthTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/HostBoundPubKeyAuthTest.java
index 9f91609aa..7cda5f037 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/HostBoundPubKeyAuthTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/HostBoundPubKeyAuthTest.java
@@ -18,12 +18,18 @@
  */
 package org.apache.sshd.client.auth.pubkey;
 
+import java.security.PublicKey;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.config.DefaultNewHostKeysHandler;
 import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.kex.extension.DefaultClientKexExtensionHandler;
 import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
 import org.apache.sshd.util.test.BaseTestSupport;
@@ -104,19 +110,38 @@ public class HostBoundPubKeyAuthTest extends 
BaseTestSupport {
 
     @MethodSource("privateKeyParams")
     @ParameterizedTest(name = "{0}")
-    public void pubkeyAuth(String privateKeyName) throws Exception {
+    void pubkeyAuth(String privateKeyName) throws Exception {
         initHostBoundPubKeyAuthTest(privateKeyName);
         FileKeyPairProvider keyPairProvider = 
CommonTestSupportUtils.createTestKeyPairProvider(getPrivateKeyResource());
         SshClient client = setupTestClient();
         client.setKeyIdentityProvider(keyPairProvider);
-        client.start();
+        CountDownLatch haveHostKeys = new CountDownLatch(1);
+        client.setNewHostKeysHandler(new DefaultNewHostKeysHandler() {
 
+            @Override
+            public void receiveNewHostKeys(ClientSession session, 
Collection<PublicKey> hostKeys) {
+                super.receiveNewHostKeys(session, hostKeys);
+                haveHostKeys.countDown();
+            }
+        });
+        client.start();
         Integer actualPort = sshdContainer.getMappedPort(22);
         String actualHost = sshdContainer.getHost();
         try (ClientSession session = client.connect("bob", actualHost, 
actualPort).verify(CONNECT_TIMEOUT).getSession()) {
             session.auth().verify(AUTH_TIMEOUT);
             assertEquals(Integer.valueOf(0), 
session.getAttribute(DefaultClientKexExtensionHandler.HOSTBOUND_AUTHENTICATION));
             checkLog(sshdContainer.getLogs());
+            // We know that this OpenSSH generates three server host keys, and 
it does send the
+            // "hostkeys...@openssh.com" global request.
+            assertTrue(haveHostKeys.await(CONNECT_TIMEOUT.toMillis(), 
TimeUnit.MILLISECONDS));
+            Collection<String> knownHostKeys = session.getRegisteredHostKeys();
+            assertNotNull(knownHostKeys);
+            knownHostKeys.forEach(s -> LOG.info("Known host key " + s));
+            assertEquals(3, knownHostKeys.size());
+            PublicKey hostKey = session.getServerKey();
+            assertNotNull(hostKey);
+            String serialized = PublicKeyEntry.toString(hostKey);
+            assertTrue(knownHostKeys.contains(serialized));
         } finally {
             client.stop();
         }
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/common/global/OpenSshHostKeysHandlerTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/common/global/OpenSshHostKeysHandlerTest.java
index 4e16801af..e71b8f1db 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/common/global/OpenSshHostKeysHandlerTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/common/global/OpenSshHostKeysHandlerTest.java
@@ -74,7 +74,7 @@ class OpenSshHostKeysHandlerTest extends BaseTestSupport {
                 = new org.apache.sshd.client.global.OpenSshHostKeysHandler() {
                     @Override
                     protected Result handleHostKeys(
-                            Session session, Collection<? extends PublicKey> 
keys, boolean wantReply,
+                            Session session, Collection<PublicKey> keys, 
boolean wantReply,
                             Buffer buffer) throws Exception {
                         handlerCalled[0] = true;
                         assertEquals(2, keys.size(), "Unexpected number of 
keys");
@@ -95,7 +95,7 @@ class OpenSshHostKeysHandlerTest extends BaseTestSupport {
                 = new org.apache.sshd.server.global.OpenSshHostKeysHandler() {
                     @Override
                     protected Result handleHostKeys(
-                            Session session, Collection<? extends PublicKey> 
keys, boolean wantReply,
+                            Session session, Collection<PublicKey> keys, 
boolean wantReply,
                             Buffer buffer) throws Exception {
                         handlerCalled[0] = true;
                         return Result.Replied;
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/common/session/GlobalRequestTest.java 
b/sshd-core/src/test/java/org/apache/sshd/common/session/GlobalRequestTest.java
index 9733e469b..80bd423c6 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/common/session/GlobalRequestTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/common/session/GlobalRequestTest.java
@@ -268,9 +268,7 @@ class GlobalRequestTest extends BaseTestSupport {
         client.setGlobalRequestHandlers(Collections.singletonList(new 
OpenSshHostKeysHandler() {
 
             @Override
-            protected Result handleHostKeys(
-                    Session session, Collection<? extends PublicKey> keys, 
boolean wantReply,
-                    Buffer buffer)
+            protected Result handleHostKeys(Session session, 
Collection<PublicKey> keys, boolean wantReply, Buffer buffer)
                     throws Exception {
                 ValidateUtils.checkTrue(!wantReply, "Unexpected reply required 
for the host keys of %s", session);
                 assertFalse(GenericUtils.isEmpty(keys));
@@ -369,9 +367,7 @@ class GlobalRequestTest extends BaseTestSupport {
         client.setGlobalRequestHandlers(Collections.singletonList(new 
OpenSshHostKeysHandler() {
 
             @Override
-            protected Result handleHostKeys(
-                    Session session, Collection<? extends PublicKey> keys, 
boolean wantReply,
-                    Buffer buffer)
+            protected Result handleHostKeys(Session session, 
Collection<PublicKey> keys, boolean wantReply, Buffer buffer)
                     throws Exception {
                 ValidateUtils.checkTrue(!wantReply, "Unexpected reply required 
for the host keys of %s", session);
                 assertFalse(GenericUtils.isEmpty(keys));

Reply via email to