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

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

commit 84196d2bef1444048645787caaa2764b54dca0cc
Author: Lyor Goldstein <[email protected]>
AuthorDate: Wed Feb 13 15:38:51 2019 +0200

    [SSHD-896] Add support for KEX extension negotiation
---
 CHANGES.md                                         |   6 +-
 README.md                                          |  24 +++
 docs/event-listeners.md                            |   6 +-
 .../java/org/apache/sshd/common/SshConstants.java  |   1 +
 .../org/apache/sshd/common/cipher/ECCurves.java    |   2 +-
 .../common/kex/extension/KexExtensionParser.java   |  52 ++++++
 .../sshd/common/kex/extension/KexExtensions.java   | 192 +++++++++++++++++++++
 .../parser/AbstractKexExtensionParser.java         |  54 ++++++
 .../kex/extension/parser/DelayCompression.java     |  52 ++++++
 .../parser/DelayedCompressionAlgorithms.java       |  95 ++++++++++
 .../common/kex/extension/parser/Elevation.java     |  48 ++++++
 .../common/kex/extension/parser/NoFlowControl.java |  48 ++++++
 .../parser/ServerSignatureAlgorithms.java          |  49 ++++++
 .../org/apache/sshd/common/util/GenericUtils.java  |  85 ++++++++-
 .../org/apache/sshd/common/util/buffer/Buffer.java | 103 +++++++++++
 .../sshd/common/util/buffer/BufferUtils.java       |  31 ++--
 .../sshd/common/util/functors/UnaryEquator.java    | 119 +++++++++++++
 .../auth/keyboard/UserAuthKeyboardInteractive.java |   4 +-
 .../sshd/client/auth/keyboard/UserInteraction.java |   2 +-
 .../org/apache/sshd/common/channel/Channel.java    |   2 +-
 .../sshd/common/kex/AbstractKexFactoryManager.java |  14 ++
 .../apache/sshd/common/kex/KexFactoryManager.java  |   3 +-
 .../common/kex/extension/KexExtensionHandler.java  | 102 +++++++++++
 .../kex/extension/KexExtensionHandlerManager.java  |  31 ++++
 .../session/ReservedSessionMessagesHandler.java    |   3 +-
 .../common/session/SessionDisconnectHandler.java   |  25 +++
 .../common/session/helpers/AbstractSession.java    | 147 ++++++++++++++--
 .../sshd/common/session/helpers/SessionHelper.java |  28 +--
 .../auth/hostbased/HostBasedAuthenticator.java     |   2 +-
 .../server/auth/keyboard/InteractiveChallenge.java |   2 +-
 .../keyboard/KeyboardInteractiveAuthenticator.java |   2 +-
 .../auth/keyboard/UserAuthKeyboardInteractive.java |   2 +-
 .../password/PasswordChangeRequiredException.java  |   2 +-
 .../sshd/server/session/AbstractServerSession.java |  17 ++
 .../sshd/common/kex/KexFactoryManagerTest.java     |  12 ++
 .../kex/extension/KexExtensionHandlerTest.java     | 104 +++++++++++
 .../java/org/apache/sshd/server/ServerTest.java    |   6 +-
 37 files changed, 1400 insertions(+), 77 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 3c76f48..07b4430 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -16,6 +16,8 @@ and also return an `Iterable<Path>`.
 * The SFTP command line client provides a `kex` command that displays the KEX 
parameters of the
 current sesssion - client/server proposals and what has been negotiated.
 
+* The `Session` object provides a `KexExtensionHandler` for usage with [KEX 
extension negotiation](https://tools.wordtothewise.com/rfc/rfc8308)
+
 ## Behavioral changes and enhancements
 
 * [SSHD-882](https://issues.apache.org/jira/browse/SSHD-882) - Provide hooks 
to allow users to register a consumer
@@ -24,4 +26,6 @@ for STDERR data sent via the `ChannelSession` - especially 
for the SFTP subsyste
 * [SSHD=892](https://issues.apache.org/jira/browse/SSHD-882) - Inform user 
about possible session disconnect prior
 to disconnecting and allow intervention via `SessionDisconnectHandler`.
 
-* [SSHD-893] Using Path(s) instead of String(s) as DirectoryScanner results
+* [SSHD-893](https://issues.apache.org/jira/browse/SSHD-893) - Using Path(s) 
instead of String(s) as DirectoryScanner results
+
+* [SSHD-896](https://issues.apache.org/jira/browse/SSHD-896) - Added support 
for [KEX extension negotiation](https://tools.wordtothewise.com/rfc/rfc8308)
diff --git a/README.md b/README.md
index 22f24c0..5e2d881 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,30 @@ leverage [Apache MINA](http://mina.apache.org), a scalable and 
high performance
 aim at being a replacement for the SSH client or SSH server from Unix 
operating systems, but rather provides support for Java
 based applications requiring SSH support.
 
+# Supported standards
+
+* [RFC 4251 - The Secure Shell (SSH) Protocol 
Architecture](https://tools.ietf.org/html/rfc4251)
+* [RFC 4252 - The Secure Shell (SSH) Authentication 
Protocol](https://tools.ietf.org/html/rfc4252)
+* [RFC 4253 - The Secure Shell (SSH) Transport Layer 
Protocol](https://tools.ietf.org/html/rfc4253)
+* [RFC 4254 - The Secure Shell (SSH) Connection 
Protocol](https://tools.ietf.org/html/rfc4254)
+* [RFC 4256 - Generic Message Exchange Authentication for the Secure Shell 
Protocol (SSH)](https://tools.ietf.org/html/rfc4256)
+* [RFC 5480 - Elliptic Curve Cryptography Subject Public Key 
Information](https://tools.ietf.org/html/rfc5480)
+* [RFC 8308 - Extension Negotiation in the Secure Shell (SSH) 
Protocol](https://tools.ietf.org/html/rfc8308)
+    * **Note:** - the code contains **hooks** for implementing the RFC but 
beyond allowing convenient
+    access to the required protocol details, it does not implement any logic 
that handles the messages.
+* SFTP version 3-6 + extensions
+    * `supported` - [DRAFT 05 - section 
4.4](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-05.tx)
+    * `supported2` - [DRAFT 13 section 
5.4](https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-10)
+    * `versions` - [DRAFT 09 Section 
4.6](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt)
+    * `vendor-id` - [DRAFT 09 - section 
4.4](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt)
+    * `acl-supported` - [DRAFT 11 - section 
5.4](https://tools.ietf.org/html/draft-ietf-secsh-filexfer-11)
+    * `newline` - [DRAFT 09 Section 
4.3](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt)
+    * `md5-hash`, `md5-hash-handle` - [DRAFT 09 - section 
9.1.1](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt)
+    * `check-file-handle`, `check-file-name` - [DRAFT 09 - section 
9.1.2](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt)
+    * `copy-file`, `copy-data` - [DRAFT 00 - sections 6, 
7](http://tools.ietf.org/id/draft-ietf-secsh-filexfer-extensions-00.txt)
+    * `space-available` - [DRAFT 09 - section 
9.3](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt)
+    * Several [OpenSSH SFTP 
extensions](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)
+
 # [Release notes](./CHANGES.md)
 
 # Core requirements
diff --git a/docs/event-listeners.md b/docs/event-listeners.md
index 3b9beef..dd6230e 100644
--- a/docs/event-listeners.md
+++ b/docs/event-listeners.md
@@ -67,7 +67,6 @@ In this context, it is worth mentioning that one can attach 
to sessions **arbitr
 
 ### `ChannelListener`
 
-
 Informs about channel related events - as with sessions, once can influence 
the channel to some extent, depending on the channel's **state**.
 The ability to influence channels is much more limited than sessions. In this 
context, it is worth mentioning that one can attach to channels
 **arbitrary attributes** that can be retrieved by the user's code later on - 
same was as it is done for sessions.
@@ -75,12 +74,15 @@ The ability to influence channels is much more limited than 
sessions. In this co
 
 ### `UnknownChannelReferenceHandler`
 
-
 Invoked whenever a message intended for an unknown channel is received. By 
default, the code **ignores** the vast majority of such messages
 and logs them at DEBUG level. For a select few types of messages the code 
generates an `SSH_CHANNEL_MSG_FAILURE` packet that is sent to the
 peer session - see `DefaultUnknownChannelReferenceHandler` implementation. The 
user may register handlers at any level - client/server, session
 and/or connection service - the one registered "closest" to connection service 
will be used.
 
+### `KexExtensionHandler`
+
+Provides hook for implementing [KEX extension 
negotiation](https://tools.wordtothewise.com/rfc/rfc8308)
+
 ### `ReservedSessionMessagesHandler`
 
 Can be used to handle the following cases:
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java 
b/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java
index 57f3c17..7e73edf 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java
@@ -45,6 +45,7 @@ public final class SshConstants {
     public static final byte SSH_MSG_DEBUG = 4;
     public static final byte SSH_MSG_SERVICE_REQUEST = 5;
     public static final byte SSH_MSG_SERVICE_ACCEPT = 6;
+
     public static final byte SSH_MSG_KEXINIT = 20;
     public static final int MSG_KEX_COOKIE_SIZE = 16;
     public static final byte SSH_MSG_NEWKEYS = 21;
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/cipher/ECCurves.java 
b/sshd-common/src/main/java/org/apache/sshd/common/cipher/ECCurves.java
index 3ccb671..2102cfa 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/cipher/ECCurves.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/cipher/ECCurves.java
@@ -434,7 +434,7 @@ public enum ECCurves implements KeyTypeIndicator, 
KeySizeIndicator, NamedResourc
      * The various {@link ECPoint} representation compression indicators
      *
      * @author <a href="mailto:[email protected]";>Apache MINA SSHD 
Project</a>
-     * @see <A HREF="https://www.ietf.org/rfc/rfc5480.txt";>RFC-5480 - section 
2.2</A>
+     * @see <A HREF="https://tools.ietf.org/html/rfc5480#section-2.2";>RFC-5480 
- section 2.2</A>
      */
     public enum ECPointCompression {
         // see http://tools.ietf.org/html/draft-jivsov-ecc-compact-00
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionParser.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionParser.java
new file mode 100644
index 0000000..6f70960
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionParser.java
@@ -0,0 +1,52 @@
+/*
+ * 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.common.kex.extension;
+
+import java.io.IOException;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+
+/**
+ * Parses a known KEX extension
+ *
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public interface KexExtensionParser<T> extends NamedResource {
+    default T parseExtension(byte[] data) throws IOException {
+        return parseExtension(data, 0, data.length);
+    }
+
+    default T parseExtension(byte[] data, int off, int len) throws IOException 
{
+        return parseExtension(new ByteArrayBuffer(data, off, len));
+    }
+
+    T parseExtension(Buffer buffer) throws IOException;
+
+    /**
+     * Adds the name + value to the buffer
+     *
+     * @param value The value of the extension
+     * @param buffer The target {@link Buffer}
+     * @throws IOException If failed to encode
+     */
+    void putExtension(T value, Buffer buffer) throws IOException;
+}
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java
new file mode 100644
index 0000000..41fbce0
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java
@@ -0,0 +1,192 @@
+/*
+ * 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.common.kex.extension;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.NavigableSet;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.kex.extension.parser.DelayCompression;
+import org.apache.sshd.common.kex.extension.parser.Elevation;
+import org.apache.sshd.common.kex.extension.parser.NoFlowControl;
+import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Readable;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * Provides some helpers for <A HREF="https://tools.ietf.org/html/rfc8308";>RFC 
8308</A>
+ *
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public final class KexExtensions {
+    public static final byte SSH_MSG_EXT_INFO = 7;
+    public static final byte SSH_MSG_NEWCOMPRESS = 8;
+
+    public static final String CLIENT_KEX_EXTENSION = "ext-info-c";
+    public static final String SERVER_KEX_EXTENSION = "ext-info-s";
+
+    /**
+     * A case <U>insensitive</U> map of all the default known {@link 
KexExtensionParser}
+     * where key=the extension name
+     */
+    private static final NavigableMap<String, KexExtensionParser<?>> 
EXTENSION_PARSERS =
+        Stream.of(
+            ServerSignatureAlgorithms.INSTANCE,
+            NoFlowControl.INSTANCE,
+            Elevation.INSTANCE,
+            DelayCompression.INSTANCE)
+        .collect(Collectors.toMap(
+            NamedResource::getName, Function.identity(),
+            GenericUtils.throwingMerger(), () -> new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
+
+    private KexExtensions() {
+        throw new UnsupportedOperationException("No instance allowed");
+    }
+
+    /**
+     * @return A case <U>insensitive</U> copy of the currently registered
+     * {@link KexExtensionParser}s names
+     */
+    public static NavigableSet<String> getRegisteredExtensionParserNames() {
+        synchronized (EXTENSION_PARSERS) {
+            return EXTENSION_PARSERS.isEmpty()
+                 ? Collections.emptyNavigableSet()
+                 : GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, 
EXTENSION_PARSERS.keySet());
+        }
+    }
+
+    /**
+     * @param name The (never {@code null}/empty) extension name
+     * @return The registered {@code KexExtensionParser} for the (case 
<U>insensitive</U>)
+     * extension name - {@code null} if no match found
+     */
+    public static KexExtensionParser<?> getRegisteredExtensionParser(String 
name) {
+        ValidateUtils.checkNotNullAndNotEmpty(name, "No extension name 
provided");
+        synchronized (EXTENSION_PARSERS) {
+            return EXTENSION_PARSERS.get(name);
+        }
+    }
+
+    /**
+     * Registers a {@link KexExtensionParser} for a named extension
+     *
+     * @param parser The (never {@code null}) parser to register
+     * @return The replaced parser for the named extension (case 
<U>insensitive</U>)
+     * - {@code null} if no previous parser registered for this extension
+     */
+    public static KexExtensionParser<?> 
registerExtensionParser(KexExtensionParser<?> parser) {
+        Objects.requireNonNull(parser, "No parser provided");
+        String name = ValidateUtils.checkNotNullAndNotEmpty(parser.getName(), 
"No extension name provided");
+        synchronized (EXTENSION_PARSERS) {
+            return EXTENSION_PARSERS.put(name, parser);
+        }
+    }
+
+    /**
+     * Registers {@link KexExtensionParser} for a named extension
+     *
+     * @param name The (never {@code null}/empty) extension name
+     * @return The removed {@code KexExtensionParser} for the (case 
<U>insensitive</U>)
+     * extension name - {@code null} if no match found
+     */
+    public static KexExtensionParser<?> unregisterExtensionParser(String name) 
{
+        ValidateUtils.checkNotNullAndNotEmpty(name, "No extension name 
provided");
+        synchronized (EXTENSION_PARSERS) {
+            return EXTENSION_PARSERS.remove(name);
+        }
+    }
+
+    /**
+     * Attempts to parse an {@code SSH_MSG_EXT_INFO} message
+     *
+     * @param buffer The {@link Buffer} containing the message
+     * @return A {@link List} of key/value &quot;pairs&quot; where key=the 
extension
+     * name, value=the parsed value using the matching registered {@link 
KexExtensionParser}.
+     * If no such parser found then the raw value bytes are set as the 
extension value.
+     * @throws IOException If failed to parse one of the extensions
+     * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-2.3";>RFC-8308 
- section 2.3</A>
+     */
+    public static List<Map.Entry<String, ?>> parseExtensions(Buffer buffer) 
throws IOException {
+        int count = buffer.getInt();
+        if (count == 0) {
+            return Collections.emptyList();
+        }
+
+        List<Map.Entry<String, ?>> entries = new ArrayList<>(count);
+        for (int index = 0; index < count; index++) {
+            String name = buffer.getString();
+            byte[] data = buffer.getBytes();
+            KexExtensionParser<?> parser = getRegisteredExtensionParser(name);
+            Object value = (parser == null) ? data : 
parser.parseExtension(data);
+            entries.add(new SimpleImmutableEntry<>(name, value));
+        }
+
+        return entries;
+    }
+
+    /**
+     * Creates an {@code SSH_MSG_EXT_INFO} message using the provided 
extensions.
+     *
+     * @param exts A {@link Collection} of key/value &quot;pairs&quot; where 
key=the extension
+     * name, value=the extension value. <B>Note:</B> if a registered {@link 
KexExtensionParser}
+     * exists for the name, then it is assumed that the value is of the 
correct type. If
+     * no registered parser found the value is assumed to be either the 
encoded value as an
+     * array of bytes or as another {@link Readable} (e.g., another {@link 
Buffer})
+     * or a {@link ByteBuffer}.
+     * @param buffer The target {@link Buffer} - assumed to already contain the
+     *  {@code SSH_MSG_EXT_INFO} opcode
+     * @throws IOException If failed to encode
+     */
+    public static void putExtensions(Collection<? extends Map.Entry<String, 
?>> exts, Buffer buffer) throws IOException {
+        int count = GenericUtils.size(exts);
+        buffer.putInt(count);
+        if (count <= 0) {
+            return;
+        }
+
+        for (Map.Entry<String, ?> ee : exts) {
+            String name = ee.getKey();
+            Object value = ee.getValue();
+            @SuppressWarnings("unchecked")
+            KexExtensionParser<Object> parser =
+                (KexExtensionParser<Object>) 
getRegisteredExtensionParser(name);
+            if (parser != null) {
+                parser.putExtension(value, buffer);
+            } else {
+                buffer.putOptionalBufferedData(value);
+            }
+        }
+    }
+}
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/AbstractKexExtensionParser.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/AbstractKexExtensionParser.java
new file mode 100644
index 0000000..1d1a3ba
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/AbstractKexExtensionParser.java
@@ -0,0 +1,54 @@
+/*
+ * 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.common.kex.extension.parser;
+
+import java.io.IOException;
+
+import org.apache.sshd.common.kex.extension.KexExtensionParser;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractKexExtensionParser<T> implements 
KexExtensionParser<T> {
+    private final String name;
+
+    protected AbstractKexExtensionParser(String name) {
+        this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No name 
provided");
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public void putExtension(T value, Buffer buffer) throws IOException {
+        buffer.putString(getName());
+        int lenPos = buffer.wpos();
+        buffer.putInt(0);   // placeholder for the encoded value length
+        encode(value, buffer);
+        BufferUtils.updateLengthPlaceholder(buffer, lenPos);
+    }
+
+    protected abstract void encode(T value, Buffer buffer) throws IOException;
+}
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayCompression.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayCompression.java
new file mode 100644
index 0000000..21763f8
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayCompression.java
@@ -0,0 +1,52 @@
+/*
+ * 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.common.kex.extension.parser;
+
+import java.io.IOException;
+
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-3.2";>RFC-8308 - 
section 3.2</A>
+ */
+public class DelayCompression extends 
AbstractKexExtensionParser<DelayedCompressionAlgorithms> {
+    public static final String NAME = "delay-compression";
+
+    public static final DelayCompression INSTANCE = new DelayCompression();
+
+    public DelayCompression() {
+        super(NAME);
+    }
+
+    @Override
+    public DelayedCompressionAlgorithms parseExtension(Buffer buffer) throws 
IOException {
+        DelayedCompressionAlgorithms algos = new 
DelayedCompressionAlgorithms();
+        algos.setClient2Server(buffer.getNameList());
+        algos.setServer2Client(buffer.getNameList());
+        return algos;
+    }
+
+    @Override
+    protected void encode(DelayedCompressionAlgorithms algos, Buffer buffer) 
throws IOException {
+        buffer.putNameList(algos.getClient2Server());
+        buffer.putNameList(algos.getServer2Client());
+    }
+}
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayedCompressionAlgorithms.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayedCompressionAlgorithms.java
new file mode 100644
index 0000000..7c01dbb
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayedCompressionAlgorithms.java
@@ -0,0 +1,95 @@
+/*
+ * 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.common.kex.extension.parser;
+
+import java.util.List;
+
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-3.2";>RFC-8308 - 
section 3.2</A>
+ */
+public class DelayedCompressionAlgorithms {
+    private List<String> client2server;
+    private List<String> server2client;
+
+    public DelayedCompressionAlgorithms() {
+        super();
+    }
+
+    public List<String> getClient2Server() {
+        return client2server;
+    }
+
+    public DelayedCompressionAlgorithms withClient2Server(List<String> 
client2server) {
+        setClient2Server(client2server);
+        return this;
+    }
+
+    public void setClient2Server(List<String> client2server) {
+        this.client2server = client2server;
+    }
+
+    public List<String> getServer2Client() {
+        return server2client;
+    }
+
+    public DelayedCompressionAlgorithms withServer2Client(List<String> 
server2client) {
+        setServer2Client(server2client);
+        return this;
+    }
+
+    public void setServer2Client(List<String> server2client) {
+        this.server2client = server2client;
+    }
+
+    @Override
+    public int hashCode() {
+        // Order might differ
+        return 31 * GenericUtils.size(getClient2Server())
+             + 37 * GenericUtils.size(getServer2Client());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        DelayedCompressionAlgorithms other = (DelayedCompressionAlgorithms) 
obj;
+        return (GenericUtils.findFirstDifferentValueIndex(getClient2Server(), 
other.getClient2Server()) < 0)
+            && (GenericUtils.findFirstDifferentValueIndex(getServer2Client(), 
other.getServer2Client()) < 0);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName()
+            + "[client2server=" + getClient2Server()
+            + ", server2client=" + getServer2Client()
+            + "]";
+    }
+}
\ No newline at end of file
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/Elevation.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/Elevation.java
new file mode 100644
index 0000000..abaa15c
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/Elevation.java
@@ -0,0 +1,48 @@
+/*
+ * 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.common.kex.extension.parser;
+
+import java.io.IOException;
+
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-3.4";>RFC-8308 - 
section 3.4</A>
+ */
+public class Elevation extends AbstractKexExtensionParser<String> {
+    public static final String NAME = "elevation";
+
+    public static final Elevation INSTANCE = new Elevation();
+
+    public Elevation() {
+        super(NAME);
+    }
+
+    @Override
+    public String parseExtension(Buffer buffer) throws IOException {
+        return buffer.getString();
+    }
+
+    @Override
+    protected void encode(String value, Buffer buffer) throws IOException {
+        buffer.putString(value);
+    }
+}
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/NoFlowControl.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/NoFlowControl.java
new file mode 100644
index 0000000..f883131
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/NoFlowControl.java
@@ -0,0 +1,48 @@
+/*
+ * 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.common.kex.extension.parser;
+
+import java.io.IOException;
+
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ * @see <A HREF="">https://tools.ietf.org/html/rfc8308#section-3.3";>RFC-8308 - 
section 3.3</A>
+ */
+public class NoFlowControl extends AbstractKexExtensionParser<String> {
+    public static final String NAME = "no-flow-control";
+
+    public static final NoFlowControl INSTANCE = new NoFlowControl();
+
+    public NoFlowControl() {
+        super(NAME);
+    }
+
+    @Override
+    public String parseExtension(Buffer buffer) throws IOException {
+        return buffer.getString();
+    }
+
+    @Override
+    protected void encode(String value, Buffer buffer) throws IOException {
+        buffer.putString(value);
+    }
+}
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/ServerSignatureAlgorithms.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/ServerSignatureAlgorithms.java
new file mode 100644
index 0000000..72b1364
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/ServerSignatureAlgorithms.java
@@ -0,0 +1,49 @@
+/*
+ * 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.common.kex.extension.parser;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-3.1";>RFC-8308 - 
section 3.1</A>
+ */
+public class ServerSignatureAlgorithms extends 
AbstractKexExtensionParser<List<String>> {
+    public static final String NAME = "server-sig-algs";
+
+    public static final ServerSignatureAlgorithms INSTANCE = new 
ServerSignatureAlgorithms();
+
+    public ServerSignatureAlgorithms() {
+        super(NAME);
+    }
+
+    @Override
+    public List<String> parseExtension(Buffer buffer) throws IOException {
+        return buffer.getNameList();
+    }
+
+    @Override
+    protected void encode(List<String> names, Buffer buffer) throws 
IOException {
+        buffer.putNameList(names);
+    }
+}
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 e7c0ab3..294148d 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
@@ -55,6 +55,8 @@ import java.util.stream.StreamSupport;
 import javax.management.MBeanException;
 import javax.management.ReflectionException;
 
+import org.apache.sshd.common.util.functors.UnaryEquator;
+
 /**
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
@@ -404,6 +406,72 @@ public final class GenericUtils {
         return result;
     }
 
+    public static <T> int findFirstDifferentValueIndex(List<? extends T> c1, 
List<? extends T> c2) {
+        return findFirstDifferentValueIndex(c1, c2, 
UnaryEquator.defaultEquality());
+    }
+
+    public static <T> int findFirstDifferentValueIndex(
+            List<? extends T> c1, List<? extends T> c2, UnaryEquator<? super 
T> equator) {
+        Objects.requireNonNull(equator, "No equator provided");
+
+        int l1 = size(c1);
+        int l2 = size(c2);
+        for (int index = 0, count = Math.min(l1, l2); index < count; index++) {
+            T v1 = c1.get(index);
+            T v2 = c2.get(index);
+            if (!equator.test(v1, v2)) {
+                return index;
+            }
+        }
+
+        // all common length items are equal - check length
+        if (l1 < l2) {
+            return l1;
+        } else if (l2 < l1) {
+            return l2;
+        } else {
+            return -1;
+        }
+    }
+
+    public static <T> int findFirstDifferentValueIndex(Iterable<? extends T> 
c1, Iterable<? extends T> c2) {
+        return findFirstDifferentValueIndex(c1, c2, 
UnaryEquator.defaultEquality());
+    }
+
+    public static <T> int findFirstDifferentValueIndex(
+            Iterable<? extends T> c1, Iterable<? extends T> c2, UnaryEquator<? 
super T> equator) {
+        return findFirstDifferentValueIndex(iteratorOf(c1), iteratorOf(c2), 
equator);
+    }
+
+    public static <T> int findFirstDifferentValueIndex(Iterator<? extends T> 
i1, Iterator<? extends T> i2) {
+        return findFirstDifferentValueIndex(i1, i2, 
UnaryEquator.defaultEquality());
+    }
+
+    public static <T> int findFirstDifferentValueIndex(
+            Iterator<? extends T> i1, Iterator<? extends T> i2, UnaryEquator<? 
super T> equator) {
+        Objects.requireNonNull(equator, "No equator provided");
+
+        i1 = iteratorOf(i1);
+        i2 = iteratorOf(i2);
+        for (int index = 0;; index++) {
+            if (i1.hasNext()) {
+                if (i2.hasNext()) {
+                    T v1 = i1.next();
+                    T v2 = i2.next();
+                    if (!equator.test(v1, v2)) {
+                        return index;
+                    }
+                } else {
+                    return index;
+                }
+            } else if (i2.hasNext()) {
+                return index;
+            } else {
+                return -1;  // neither has a next value - both exhausted at 
the same time
+            }
+        }
+    }
+
     public static <T> boolean containsAny(Collection<? extends T> coll, 
Iterable<? extends T> values) {
         if (isEmpty(coll)) {
             return false;
@@ -418,28 +486,29 @@ public final class GenericUtils {
         return false;
     }
 
-    public static <T> void forEach(Iterable<T> values, Consumer<T> consumer) {
+    public static <T> void forEach(Iterable<? extends T> values, Consumer<? 
super T> consumer) {
         if (isNotEmpty(values)) {
             values.forEach(consumer);
         }
     }
 
-    public static <T, U> List<U> map(Collection<T> values, Function<? super T, 
? extends U> mapper) {
+    public static <T, U> List<U> map(Collection<? extends T> values, 
Function<? super T, ? extends U> mapper) {
         return stream(values).map(mapper).collect(Collectors.toList());
     }
 
     public static <T, U> NavigableSet<U> mapSort(
-            Collection<T> values, Function<? super T, ? extends U> mapper, 
Comparator<U> comparator) {
+            Collection<? extends T> values, Function<? super T, ? extends U> 
mapper, Comparator<? super U> comparator) {
         return stream(values).map(mapper).collect(toSortedSet(comparator));
     }
 
     public static <T, K, U> NavigableMap<K, U> toSortedMap(
-            Iterable<T> values, Function<? super T, ? extends K> keyMapper, 
Function<? super T, ? extends U> valueMapper, Comparator<K> comparator) {
+            Iterable<? extends T> values, Function<? super T, ? extends K> 
keyMapper,
+            Function<? super T, ? extends U> valueMapper, Comparator<? super 
K> comparator) {
         return stream(values).collect(toSortedMap(keyMapper, valueMapper, 
comparator));
     }
 
     public static <T, K, U> Collector<T, ?, NavigableMap<K, U>> toSortedMap(
-            Function<? super T, ? extends K> keyMapper, Function<? super T, ? 
extends U> valueMapper, Comparator<K> comparator) {
+            Function<? super T, ? extends K> keyMapper, Function<? super T, ? 
extends U> valueMapper, Comparator<? super K> comparator) {
         return Collectors.toMap(keyMapper, valueMapper, throwingMerger(), () 
-> new TreeMap<>(comparator));
     }
 
@@ -449,7 +518,7 @@ public final class GenericUtils {
         };
     }
 
-    public static <T> Collector<T, ?, NavigableSet<T>> 
toSortedSet(Comparator<T> comparator) {
+    public static <T> Collector<T, ?, NavigableSet<T>> 
toSortedSet(Comparator<? super T> comparator) {
         return Collectors.toCollection(() -> new TreeSet<>(comparator));
     }
 
@@ -629,8 +698,8 @@ public final class GenericUtils {
      */
     public static <T> List<T> selectMatchingMembers(Predicate<? super T> 
acceptor, Collection<? extends T> values) {
         return GenericUtils.stream(values)
-                .filter(acceptor)
-                .collect(Collectors.toList());
+            .filter(acceptor)
+            .collect(Collectors.toList());
     }
 
     /**
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 8f0dace..5afa514 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
@@ -283,6 +283,39 @@ public abstract class Buffer implements Readable {
     }
 
     /**
+     * According to <A HREF="https://tools.ietf.org/html/rfc4251#page-10";>RFC 
4251</A>:
+     *
+     *      A name-list is represented as a uint32 containing its length 
(number of bytes
+     *      that follow) followed by a comma-separated list of zero or more 
names.
+     *
+     * @return The parsed result
+     */
+    public List<String> getNameList() {
+        return getNameList(StandardCharsets.UTF_8);
+    }
+
+    public List<String> getNameList(Charset charset) {
+        return getNameList(charset, ',');
+    }
+
+    public List<String> getNameList(char separator) {
+        return getNameList(StandardCharsets.UTF_8, separator);
+    }
+
+    /**
+     * Parses a string that contains values separated by a delimiter
+     *
+     * @param charset The {@link Charset} to use to read the string
+     * @param separator The separator
+     * @return A {@link List} of the parsed values
+     */
+    public List<String> getNameList(Charset charset, char separator) {
+        String list = getString(charset);
+        String[] values = GenericUtils.split(list, separator);
+        return GenericUtils.isEmpty(values) ? Collections.emptyList() : 
Arrays.asList(values);
+    }
+
+    /**
      * @param usePrependedLength If {@code true} then there is a 32-bit
      * value indicating the number of strings to read. Otherwise, the
      * method will use a &quot;greedy&quot; reading of strings while more
@@ -550,6 +583,46 @@ public abstract class Buffer implements Readable {
         putRawBytes(workBuf, 0, Byte.BYTES);
     }
 
+    /**
+     * Checks if the <tt>buffer</tt> argument is an array of bytes,
+     * a {@link Readable} instance or a {@link ByteBuffer} and invokes
+     * the appropriate {@code putXXX} method. If {@code null} then
+     * puts an empty byte array value
+     *
+     * @param buffer The buffered data object to inspect
+     * @see #putBufferedData(Object)
+     */
+    public void putOptionalBufferedData(Object buffer) {
+        if (buffer == null) {
+            putBytes(GenericUtils.EMPTY_BYTE_ARRAY);
+        } else {
+            putBufferedData(buffer);
+        }
+    }
+
+    /**
+     * Checks if the <tt>buffer</tt> argument is an array of bytes,
+     * a {@link Readable} instance or a {@link ByteBuffer} and invokes
+     * the appropriate {@code putXXX} method.
+     *
+     * @param buffer The (never {@code null}) buffer object to put
+     * @throws IllegalArgumentException If <tt>buffer</tt> is none of the
+     * supported types
+     */
+    public void putBufferedData(Object buffer) {
+        Objects.requireNonNull(buffer, "No buffered data to encode");
+        if (buffer instanceof byte[]) {
+            putBytes((byte[]) buffer);
+        } else if (buffer instanceof Readable) {
+            putBuffer((Readable) buffer);
+        } else if (buffer instanceof ByteBuffer) {
+            putBuffer((ByteBuffer) buffer);
+        } else {
+            throw new IllegalArgumentException("No buffered overload found for 
"
+                + ((buffer == null) ? null : buffer.getClass().getName()));
+        }
+    }
+
     public void putBuffer(Readable buffer) {
         putBuffer(buffer, true);
     }
@@ -670,6 +743,36 @@ public abstract class Buffer implements Readable {
         }
     }
 
+    /**
+     * According to <A HREF="https://tools.ietf.org/html/rfc4251#page-10";>RFC 
4251</A>:
+     *
+     *      A name-list is represented as a uint32 containing its length 
(number of bytes
+     *      that follow) followed by a comma-separated list of zero or more 
names.
+     */
+    public void putNameList(Collection<String> names) {
+        putNameList(names, StandardCharsets.UTF_8);
+    }
+
+    public void putNameList(Collection<String> names, Charset charset) {
+        putNameList(names, charset, ',');
+    }
+
+    public void putNameList(Collection<String> names, char separator) {
+        putNameList(names, StandardCharsets.UTF_8, separator);
+    }
+
+    /**
+     * Adds a string that contains values separated by a delimiter
+     *
+     * @param names The names to set
+     * @param charset The {@link Charset} to use to encode the string
+     * @param separator The separator
+     */
+    public void putNameList(Collection<String> names, Charset charset, char 
separator) {
+        String list = GenericUtils.join(names, separator);
+        putString(list, charset);
+    }
+
     public void putString(String string) {
         putString(string, StandardCharsets.UTF_8);
     }
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java 
b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
index 31387db..c7f3ed2 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
@@ -238,7 +238,9 @@ public final class BufferUtils {
      * @throws NumberFormatException If invalid HEX characters found
      * @see #decodeHex(OutputStream, char, CharSequence, int, int)
      */
-    public static <S extends OutputStream> int decodeHex(S stream, char 
separator, CharSequence csq) throws IOException {
+    public static <S extends OutputStream> int decodeHex(
+            S stream, char separator, CharSequence csq)
+                throws IOException {
         return decodeHex(stream, separator, csq, 0, GenericUtils.length(csq));
     }
 
@@ -293,7 +295,7 @@ public final class BufferUtils {
      * @param input The {@link InputStream}
      * @param buf   Work buffer to use
      * @return The read 32-bit value
-     * @throws IOException If failed to read 4 bytes or not enough room in
+     * @throws IOException If failed to read 4 bytes or not enough room in 
work buffer
      * @see #readInt(InputStream, byte[], int, int)
      */
     public static int readInt(InputStream input, byte[] buf) throws 
IOException {
@@ -308,8 +310,7 @@ public final class BufferUtils {
      * @param offset Offset in buffer to us
      * @param len    Available length - must have at least 4 bytes available
      * @return The read 32-bit value
-     * @throws IOException If failed to read 4 bytes or not enough room in
-     *                     work buffer
+     * @throws IOException If failed to read 4 bytes or not enough room in 
work buffer
      * @see #readUInt(InputStream, byte[], int, int)
      */
     public static int readInt(InputStream input, byte[] buf, int offset, int 
len) throws IOException {
@@ -322,7 +323,7 @@ public final class BufferUtils {
      * @param input The {@link InputStream}
      * @param buf   Work buffer to use
      * @return The read 32-bit value
-     * @throws IOException If failed to read 4 bytes or not enough room in
+     * @throws IOException If failed to read 4 bytes or not enough room in 
work buffer
      * @see #readUInt(InputStream, byte[], int, int)
      */
     public static long readUInt(InputStream input, byte[] buf) throws 
IOException {
@@ -337,8 +338,7 @@ public final class BufferUtils {
      * @param offset Offset in buffer to us
      * @param len    Available length - must have at least 4 bytes available
      * @return The read 32-bit value
-     * @throws IOException If failed to read 4 bytes or not enough room in
-     *                     work buffer
+     * @throws IOException If failed to read 4 bytes or not enough room in 
work buffer
      * @see #getUInt(byte[], int, int)
      */
     public static long readUInt(InputStream input, byte[] buf, int offset, int 
len) throws IOException {
@@ -357,8 +357,8 @@ public final class BufferUtils {
 
     /**
      * @param buf A buffer holding a 32-bit unsigned integer in <B>big 
endian</B>
-     *            format. <B>Note:</B> if more than 4 bytes are available, 
then only the
-     *            <U>first</U> 4 bytes in the buffer will be used
+     * format. <B>Note:</B> if more than 4 bytes are available, then only the
+     * <U>first</U> 4 bytes in the buffer will be used
      * @return The result as a {@code long} whose 32 high-order bits are zero
      * @see #getUInt(byte[], int, int)
      */
@@ -367,12 +367,11 @@ public final class BufferUtils {
     }
 
     /**
-     * @param buf A buffer holding a 32-bit unsigned integer in <B>big 
endian</B>
-     *            format.
+     * @param buf A buffer holding a 32-bit unsigned integer in <B>big 
endian</B> format.
      * @param off The offset of the data in the buffer
      * @param len The available data length. <B>Note:</B> if more than 4 bytes
-     *            are available, then only the <U>first</U> 4 bytes in the 
buffer will be
-     *            used (starting at the specified <tt>offset</tt>)
+     * are available, then only the <U>first</U> 4 bytes in the buffer will be
+     * used (starting at the specified <tt>offset</tt>)
      * @return The result as a {@code long} whose 32 high-order bits are zero
      */
     public static long getUInt(byte[] buf, int off, int len) {
@@ -393,7 +392,7 @@ public final class BufferUtils {
      * @param output The {@link OutputStream} to write the value
      * @param value  The 32-bit value
      * @param buf    A work buffer to use - must have enough space to contain 
4 bytes
-     * @throws IOException If failed to write the value or work buffer to small
+     * @throws IOException If failed to write the value or work buffer too 
small
      * @see #writeInt(OutputStream, int, byte[], int, int)
      */
     public static void writeInt(OutputStream output, int value, byte[] buf) 
throws IOException {
@@ -408,7 +407,7 @@ public final class BufferUtils {
      * @param buf    A work buffer to use - must have enough space to contain 
4 bytes
      * @param off    The offset to write the value
      * @param len    The available space
-     * @throws IOException If failed to write the value or work buffer to small
+     * @throws IOException If failed to write the value or work buffer too 
small
      * @see #writeUInt(OutputStream, long, byte[], int, int)
      */
     public static void writeInt(
@@ -423,7 +422,7 @@ public final class BufferUtils {
      * @param output The {@link OutputStream} to write the value
      * @param value  The 32-bit value
      * @param buf    A work buffer to use - must have enough space to contain 
4 bytes
-     * @throws IOException If failed to write the value or work buffer to small
+     * @throws IOException If failed to write the value or work buffer too 
small
      * @see #writeUInt(OutputStream, long, byte[], int, int)
      */
     public static void writeUInt(OutputStream output, long value, byte[] buf) 
throws IOException {
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/util/functors/UnaryEquator.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/util/functors/UnaryEquator.java
new file mode 100644
index 0000000..4834065
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/util/functors/UnaryEquator.java
@@ -0,0 +1,119 @@
+/*
+ * 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.common.util.functors;
+
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.function.BiPredicate;
+
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * Checks equality between 2 entities of same type
+ * @param <T> Type of compared entity
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface UnaryEquator<T> extends BiPredicate<T, T> {
+    /**
+     * Returns a composed equator that represents a short-circuiting logical
+     * AND of this equator and another.  When evaluating the composed
+     * equator, if this equator is {@code false}, then the {@code other}
+     * equator is not evaluated.
+     *
+     * @param other The other (never {@code null} equator
+     * @return The compound equator
+     */
+    default UnaryEquator<T> and(UnaryEquator<? super T> other) {
+        Objects.requireNonNull(other, "No other equator to compose");
+        return (t1, t2) -> this.test(t1, t2) && other.test(t1, t2);
+    }
+
+    /**
+     * Returns a composed equator that represents a short-circuiting logical
+     * AND of this equator and another.  When evaluating the composed
+     * equator, if this equator is {@code true}, then the {@code other}
+     * equator is not evaluated.
+     *
+     * @param other The other (never {@code null} equator
+     * @return The compound equator
+     */
+    default UnaryEquator<T> or(UnaryEquator<? super T> other) {
+        Objects.requireNonNull(other, "No other equator to compose");
+        return (t1, t2) -> this.test(t1, t2) || other.test(t1, t2);
+    }
+
+    /**
+     * @return an equator that represents the logical negation of this one
+     */
+    @Override
+    default UnaryEquator<T> negate() {
+        return (t1, t2) -> !this.test(t1, t2);
+    }
+
+    /**
+     * @param <T> Type of entity
+     * @return The default equality checker
+     * @see EqualityUtils#isEqualValue(Object, Object)
+     */
+    static <T> UnaryEquator<T> defaultEquality() {
+        return Objects::equals;
+    }
+
+    /**
+     * @param <T> Type of entity
+     * @return An equator that checks reference equality
+     * @see EqualityUtils#isSameReference(Object, Object)
+     */
+    static <T> UnaryEquator<T> referenceEquality() {
+        return GenericUtils::isSameReference;
+    }
+
+    /**
+     * Converts a {@link Comparator} into a {@link UnaryEquator} that
+     * returns {@code true} if the comparator returns zero
+     *
+     * @param <T> Type of entity
+     * @param c The (never {@code null}) comparator
+     * @return The equivalent equator
+     */
+    static <T> UnaryEquator<T> comparing(Comparator<? super T> c) {
+        Objects.requireNonNull(c, "No comparator");
+        return (o1, o2) -> c.compare(o1, o2) == 0;
+    }
+
+    /**
+     * @param <T> Type of evaluated entity
+     * @return A {@link UnaryEquator} that returns always {@code true}
+     * @see <A HREF="https://en.wikipedia.org/wiki/Tee_(symbol)">verum</A>
+     */
+    static <T> UnaryEquator<T> verum() {
+        return (o1, o2) -> true;
+    }
+
+    /**
+     * @param <T> Type of evaluated entity
+     * @return A {@link UnaryEquator} that returns always {@code false}
+     * @see <A HREF="https://en.wikipedia.org/wiki/Up_tack";>falsum</A>
+     */
+    static <T> UnaryEquator<T> falsum() {
+        return (o1, o2) -> false;
+    }
+}
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserAuthKeyboardInteractive.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserAuthKeyboardInteractive.java
index 96a204f..677906e 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserAuthKeyboardInteractive.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserAuthKeyboardInteractive.java
@@ -33,7 +33,7 @@ import org.apache.sshd.common.util.buffer.Buffer;
 
 /**
  * Manages a &quot;keyboard-interactive&quot; exchange according to
- * <A HREF="https://www.ietf.org/rfc/rfc4256.txt";>RFC4256</A>
+ * <A HREF="https://tools.ietf.org/html/rfc4256";>RFC4256</A>
  *
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
@@ -249,7 +249,7 @@ public class UserAuthKeyboardInteractive extends 
AbstractUserAuth {
      *                    length as the prompts
      * @return The response for each prompt - if {@code null} then the 
assumption
      * is that some internal error occurred and no response is sent. 
<B>Note:</B>
-     * according to <A HREF="https://www.ietf.org/rfc/rfc4256.txt";>RFC4256</A>
+     * according to <A HREF="https://tools.ietf.org/html/rfc4256";>RFC4256</A>
      * the number of responses should be <U>exactly</U> the same as the number
      * of prompts. However, since it is the <U>server's</U> responsibility to
      * enforce this we do not validate the response (other than logging it as
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java
index 56f5e1d..5c294fa 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java
@@ -26,7 +26,7 @@ import org.apache.sshd.client.session.ClientSession;
  * Interface used by the ssh client to communicate with the end user.
  *
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
- * @see <a href="https://www.ietf.org/rfc/rfc4256.txt";>RFC 4256</A>
+ * @see <a href="https://tools.ietf.org/html/rfc4256";>RFC 4256</A>
  */
 public interface UserInteraction {
     /**
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/channel/Channel.java 
b/sshd-core/src/main/java/org/apache/sshd/common/channel/Channel.java
index 4fe1dbe..4f89ce8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/channel/Channel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/channel/Channel.java
@@ -169,7 +169,7 @@ public interface Channel
     /**
      * @return {@code true} if the peer signaled that it will not send any
      * more data
-     * @see <A HREF="https://www.ietf.org/rfc/rfc4254.txt";>RFC 4254 - section 
5.3 - SSH_MSG_CHANNEL_EOE</A>
+     * @see <A HREF="https://tools.ietf.org/html/rfc4254#section-5.3";>RFC 4254 
- section 5.3 - SSH_MSG_CHANNEL_EOF</A>
      */
     boolean isEofSignalled();
 
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractKexFactoryManager.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractKexFactoryManager.java
index eb502e4..da4ed0e 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractKexFactoryManager.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractKexFactoryManager.java
@@ -25,6 +25,7 @@ import java.util.List;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.cipher.Cipher;
 import org.apache.sshd.common.compression.Compression;
+import org.apache.sshd.common.kex.extension.KexExtensionHandler;
 import org.apache.sshd.common.mac.Mac;
 import org.apache.sshd.common.signature.Signature;
 import org.apache.sshd.common.util.GenericUtils;
@@ -42,6 +43,7 @@ public abstract class AbstractKexFactoryManager
     private List<NamedFactory<Compression>> compressionFactories;
     private List<NamedFactory<Mac>> macFactories;
     private List<NamedFactory<Signature>> signatureFactories;
+    private KexExtensionHandler kexExtensionHandler;
 
     protected AbstractKexFactoryManager() {
         this(null);
@@ -115,6 +117,18 @@ public abstract class AbstractKexFactoryManager
         this.signatureFactories = signatureFactories;
     }
 
+    @Override
+    public KexExtensionHandler getKexExtensionHandler() {
+        KexFactoryManager parent = getDelegate();
+        return resolveEffectiveProvider(
+            KexExtensionHandler.class, kexExtensionHandler, (parent == null) ? 
null : parent.getKexExtensionHandler());
+    }
+
+    @Override
+    public void setKexExtensionHandler(KexExtensionHandler 
kexExtensionHandler) {
+        this.kexExtensionHandler = kexExtensionHandler;
+    }
+
     protected <V> List<NamedFactory<V>> resolveEffectiveFactories(
             Class<V> factoryType, List<NamedFactory<V>> local, 
List<NamedFactory<V>> inherited) {
         if (GenericUtils.isEmpty(local)) {
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/kex/KexFactoryManager.java 
b/sshd-core/src/main/java/org/apache/sshd/common/kex/KexFactoryManager.java
index 6e46154..6c873d2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/kex/KexFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/KexFactoryManager.java
@@ -30,6 +30,7 @@ import org.apache.sshd.common.cipher.BuiltinCiphers;
 import org.apache.sshd.common.cipher.Cipher;
 import org.apache.sshd.common.compression.BuiltinCompressions;
 import org.apache.sshd.common.compression.Compression;
+import org.apache.sshd.common.kex.extension.KexExtensionHandlerManager;
 import org.apache.sshd.common.mac.BuiltinMacs;
 import org.apache.sshd.common.mac.Mac;
 import org.apache.sshd.common.signature.SignatureFactoriesManager;
@@ -40,7 +41,7 @@ import org.apache.sshd.common.util.ValidateUtils;
  * Holds KEX negotiation stage configuration
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
-public interface KexFactoryManager extends SignatureFactoriesManager {
+public interface KexFactoryManager extends SignatureFactoriesManager, 
KexExtensionHandlerManager {
     /**
      * Retrieve the list of named factories for <code>KeyExchange</code>.
      *
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java
new file mode 100644
index 0000000..0464bc3
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java
@@ -0,0 +1,102 @@
+/*
+ * 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.common.kex.extension;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * Used to support <A HREF="https://tools.ietf.org/html/rfc8308";>RFC 8308</A>
+ *
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public interface KexExtensionHandler {
+    enum KexPhase {
+        NEWKEYS,
+        AUTHOK;
+
+        public static final Set<KexPhase> VALUES =
+            Collections.unmodifiableSet(EnumSet.allOf(KexPhase.class));
+    }
+
+    /**
+     * @param session The {@link Session} about to execute KEX
+     * @return {@code true} whether to declare KEX extensions availability for 
the session
+     * @throws IOException If failed to process the request
+     */
+    default boolean isKexExtensionsAvailable(Session session) throws 
IOException {
+        return true;
+    }
+
+    /**
+     * Invoked in order to allow the handler to send an {@code 
SSH_MSG_EXT_INFO} message.
+     *
+     * @param session The {@link Session}
+     * @param phase The phase at which the handler is invoked
+     * @throws IOException If failed to handle the invocation
+     * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-2.4";>RFC-8308 
- section 2.4</A>
+     */
+    default void sendKexExtensions(Session session, KexPhase phase) throws 
IOException {
+        // do nothing
+    }
+
+    /**
+     * Parses the {@code SSH_MSG_EXT_INFO} message
+     *
+     * @param session The {@link Session} through which the message was 
received
+     * @param buffer The message buffer
+     * @throws IOException If failed to handle the message
+     * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-2.3";>RFC-8308 
- section 2.3</A>
+     * @see #handleKexExtensionRequest(Session, int, int, String, byte[])
+     */
+    default void handleKexExtensionsMessage(Session session, Buffer buffer) 
throws IOException {
+        int count = buffer.getInt();
+        for (int index = 0; index < count; index++) {
+            String name = buffer.getString();
+            byte[] data = buffer.getBytes();
+            if (!handleKexExtensionRequest(session, index, count, name, data)) 
{
+                return;
+            }
+        }
+    }
+
+    /**
+     * Invoked by {@link #handleKexExtensionsMessage(Session, Buffer)} in 
order to
+     * handle a specific extension.
+     *
+     * @param session The {@link Session} through which the message was 
received
+     * @param index The 0-based extension index
+     * @param count The total extensions in the message
+     * @param name The extension name
+     * @param data The extension data
+     * @return {@code true} whether to proceed to the next extension or
+     * stop processing the rest
+     * @throws IOException If failed to handle the extension
+     */
+    default boolean handleKexExtensionRequest(
+            Session session, int index, int count, String name, byte[] data) 
throws IOException {
+        return true;
+    }
+}
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerManager.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerManager.java
new file mode 100644
index 0000000..5eb87ef
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerManager.java
@@ -0,0 +1,31 @@
+/*
+ * 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.common.kex.extension;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public interface KexExtensionHandlerManager {
+    KexExtensionHandler getKexExtensionHandler();
+
+    void setKexExtensionHandler(KexExtensionHandler handler);
+}
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java
index 48ab970..e229eb0 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java
@@ -24,7 +24,8 @@ import org.apache.sshd.common.util.buffer.Buffer;
 
 /**
  * Provides a way to listen and handle the {@code SSH_MSG_IGNORE} and
- * {@code SSH_MSG_DEBUG} messages that are received by a session.
+ * {@code SSH_MSG_DEBUG} messages that are received by a session, as well
+ * as proprietary and/or extension messages.
  *
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java
index d1e7dab..76a0d85 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java
@@ -20,8 +20,10 @@
 package org.apache.sshd.common.session;
 
 import java.io.IOException;
+import java.util.Map;
 
 import org.apache.sshd.common.Service;
+import org.apache.sshd.common.kex.KexProposalOption;
 import org.apache.sshd.common.session.Session.TimeoutStatus;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.server.ServerFactoryManager;
@@ -129,4 +131,27 @@ public interface SessionDisconnectHandler {
                 throws IOException {
         return false;
     }
+
+    /**
+     * Invoked if after KEX negotiation parameters resolved one of the options
+     * violates some internal constraint (e.g., cannot negotiate a value, or
+     * <A HREF="https://tools.ietf.org/html/rfc8308#section-2.2";>RFC 8308 - 
section 2.2</A>).
+     *
+     * @param session The session where the violation occurred
+     * @param c2sOptions The client options
+     * @param s2cOptions The server options
+     * @param negotiatedGuess The negotiated KEX options
+     * @param option The violating {@link KexProposalOption}
+     * @return {@code true} if disregard the violation - if {@code false} then
+     * session will disconnect
+     */
+    default boolean handleKexDisconnectReason(
+            Session session, Map<KexProposalOption, String> c2sOptions, 
Map<KexProposalOption, String> s2cOptions,
+            Map<KexProposalOption, String> negotiatedGuess, KexProposalOption 
option) {
+        if (KexProposalOption.S2CLANG.equals(option) || 
KexProposalOption.C2SLANG.equals(option)) {
+            return true;    // OK if cannot agree on a language
+        }
+
+        return false;
+    }
 }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
index ef345fe..6a26457 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
@@ -63,10 +63,14 @@ import org.apache.sshd.common.io.IoWriteFuture;
 import org.apache.sshd.common.kex.KexProposalOption;
 import org.apache.sshd.common.kex.KexState;
 import org.apache.sshd.common.kex.KeyExchange;
+import org.apache.sshd.common.kex.extension.KexExtensionHandler;
+import org.apache.sshd.common.kex.extension.KexExtensionHandler.KexPhase;
+import org.apache.sshd.common.kex.extension.KexExtensions;
 import org.apache.sshd.common.mac.Mac;
 import org.apache.sshd.common.mac.MacInformation;
 import org.apache.sshd.common.random.Random;
 import org.apache.sshd.common.session.ReservedSessionMessagesHandler;
+import org.apache.sshd.common.session.SessionDisconnectHandler;
 import org.apache.sshd.common.session.SessionListener;
 import org.apache.sshd.common.session.SessionWorkBuffer;
 import org.apache.sshd.common.util.EventListenerUtils;
@@ -407,6 +411,9 @@ public abstract class AbstractSession extends SessionHelper 
{
             case SshConstants.SSH_MSG_NEWKEYS:
                 handleNewKeys(cmd, buffer);
                 break;
+            case KexExtensions.SSH_MSG_EXT_INFO:
+                handleKexExtension(cmd, buffer);
+                break;
             default:
                 if ((cmd >= SshConstants.SSH_MSG_KEX_FIRST) && (cmd <= 
SshConstants.SSH_MSG_KEX_LAST)) {
                     if (firstKexPacketFollows != null) {
@@ -468,12 +475,54 @@ public abstract class AbstractSession extends 
SessionHelper {
         return true;
     }
 
+    /**
+     * Compares the specified {@link KexProposalOption} option value for 
client vs. server
+     *
+     * @param option The option to check
+     * @return {@code null} if option is equal, otherwise a kex/value pair 
where key=client
+     * option value and value=the server-side one
+     */
     protected SimpleImmutableEntry<String, String> 
comparePreferredKexProposalOption(KexProposalOption option) {
         String[] clientPreferences = 
GenericUtils.split(clientProposal.get(option), ',');
         String clientValue = clientPreferences[0];
         String[] serverPreferences = 
GenericUtils.split(serverProposal.get(option), ',');
         String serverValue = serverPreferences[0];
-        return clientValue.equals(serverValue) ? null : new 
SimpleImmutableEntry<>(clientValue, serverValue);
+        return Objects.equals(clientValue, serverValue) ? null : new 
SimpleImmutableEntry<>(clientValue, serverValue);
+    }
+
+    /**
+     * Send a message to put new keys into use.
+     *
+     * @return An {@link IoWriteFuture} that can be used to wait and
+     * check the result of sending the packet
+     * @throws IOException if an error occurs sending the message
+     */
+    protected IoWriteFuture sendNewKeys() throws IOException {
+        if (log.isDebugEnabled()) {
+            log.debug("sendNewKeys({}) Send SSH_MSG_NEWKEYS", this);
+        }
+
+        Buffer buffer = createBuffer(SshConstants.SSH_MSG_NEWKEYS, Byte.SIZE);
+        IoWriteFuture future = writePacket(buffer);
+        /*
+         * According to https://tools.ietf.org/html/rfc8308#section-2.4:
+         *
+         *
+         *      If a client sends SSH_MSG_EXT_INFO, it MUST send it as the 
next packet
+         *      following the client's first SSH_MSG_NEWKEYS message to the 
server.
+         *
+         *      If a server sends SSH_MSG_EXT_INFO, it MAY send it at zero, 
one, or
+         *      both of the following opportunities:
+         *
+         *          + As the next packet following the server's first 
SSH_MSG_NEWKEYS.
+         */
+        KexExtensionHandler extHandler = getKexExtensionHandler();
+        if ((extHandler == null) || 
(!extHandler.isKexExtensionsAvailable(this))) {
+            return future;
+        }
+
+        extHandler.sendKexExtensions(this, KexPhase.NEWKEYS);
+        return future;
     }
 
     protected void handleKexMessage(int cmd, Buffer buffer) throws Exception {
@@ -494,6 +543,16 @@ public abstract class AbstractSession extends 
SessionHelper {
         }
     }
 
+    protected void handleKexExtension(int cmd, Buffer buffer) throws Exception 
{
+        KexExtensionHandler extHandler = getKexExtensionHandler();
+        if ((extHandler == null) || 
(!extHandler.isKexExtensionsAvailable(this))) {
+            notImplemented(cmd, buffer);
+            return;
+        }
+
+        extHandler.handleKexExtensionsMessage(this, buffer);
+    }
+
     protected void handleServiceRequest(Buffer buffer) throws Exception {
         String serviceName = buffer.getString();
         handleServiceRequest(serviceName, buffer);
@@ -1417,8 +1476,9 @@ public abstract class AbstractSession extends 
SessionHelper {
      * the {@link #negotiationResult} property.
      *
      * @return The negotiated options {@link Map}
+     * @throws IOException If negotiation failed
      */
-    protected Map<KexProposalOption, String> negotiate() {
+    protected Map<KexProposalOption, String> negotiate() throws IOException {
         Map<KexProposalOption, String> c2sOptions = getClientKexProposals();
         Map<KexProposalOption, String> s2cOptions = getServerKexProposals();
         signalNegotiationStart(c2sOptions, s2cOptions);
@@ -1426,12 +1486,21 @@ public abstract class AbstractSession extends 
SessionHelper {
         Map<KexProposalOption, String> guess = new 
EnumMap<>(KexProposalOption.class);
         Map<KexProposalOption, String> negotiatedGuess = 
Collections.unmodifiableMap(guess);
         try {
+            boolean debugEnabled = log.isDebugEnabled();
             boolean traceEnabled = log.isTraceEnabled();
+            SessionDisconnectHandler discHandler = 
getSessionDisconnectHandler();
             for (KexProposalOption paramType : KexProposalOption.VALUES) {
                 String clientParamValue = c2sOptions.get(paramType);
                 String serverParamValue = s2cOptions.get(paramType);
                 String[] c = GenericUtils.split(clientParamValue, ',');
                 String[] s = GenericUtils.split(serverParamValue, ',');
+                /*
+                 * According to 
https://tools.ietf.org/html/rfc8308#section-2.2:
+                 *
+                 *      Implementations MAY disconnect if the counterpart 
sends an incorrect (KEX extension) indicator
+                 *
+                 * TODO - for now we do not enforce this
+                 */
                 for (String ci : c) {
                     for (String si : s) {
                         if (ci.equals(si)) {
@@ -1448,25 +1517,55 @@ public abstract class AbstractSession extends 
SessionHelper {
 
                 // check if reached an agreement
                 String value = guess.get(paramType);
-                if (value == null) {
-                    String message = "Unable to negotiate key exchange for " + 
paramType.getDescription()
-                        + " (client: " + clientParamValue + " / server: " + 
serverParamValue + ")";
-                    // OK if could not negotiate languages
-                    if (KexProposalOption.S2CLANG.equals(paramType) || 
KexProposalOption.C2SLANG.equals(paramType)) {
-                        if (traceEnabled) {
-                            log.trace("negotiate({}) {}", this, message);
-                        }
-                    } else {
-                        throw new IllegalStateException(message);
+                if (value != null) {
+                    if (traceEnabled) {
+                        log.trace("negotiate({})[{}] guess={} (client={} / 
server={})",
+                            this, paramType.getDescription(), value, 
clientParamValue, serverParamValue);
                     }
-                } else {
+                    continue;
+                }
+
+                if ((discHandler != null)
+                        && discHandler.handleKexDisconnectReason(
+                                this, c2sOptions, s2cOptions, negotiatedGuess, 
paramType)) {
+                    if (debugEnabled) {
+                        log.debug("negotiate({}) ignore missing value for KEX 
option={}", this, paramType);
+                    }
+                    continue;
+                }
+
+                String message = "Unable to negotiate key exchange for " + 
paramType.getDescription()
+                    + " (client: " + clientParamValue + " / server: " + 
serverParamValue + ")";
+                // OK if could not negotiate languages
+                if (KexProposalOption.S2CLANG.equals(paramType) || 
KexProposalOption.C2SLANG.equals(paramType)) {
                     if (traceEnabled) {
-                        log.trace("negotiate(" + this + ")[" + 
paramType.getDescription() + "] guess=" + value
-                            + " (client: " + clientParamValue + " / server: " 
+ serverParamValue + ")");
+                        log.trace("negotiate({}) {}", this, message);
+                    }
+                } else {
+                    throw new 
SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, message);
+                }
+            }
+
+            /*
+             * According to https://tools.ietf.org/html/rfc8308#section-2.2:
+             *
+             *      If "ext-info-c" or "ext-info-s" ends up being negotiated 
as a
+             *      key exchange method, the parties MUST disconnect.
+             */
+            String kexOption = guess.get(KexProposalOption.ALGORITHMS);
+            if (KexExtensions.CLIENT_KEX_EXTENSION.equalsIgnoreCase(kexOption)
+                    || 
KexExtensions.SERVER_KEX_EXTENSION.equalsIgnoreCase(kexOption)) {
+                if ((discHandler != null)
+                        && discHandler.handleKexDisconnectReason(
+                                this, c2sOptions, s2cOptions, negotiatedGuess, 
KexProposalOption.ALGORITHMS)) {
+                    if (debugEnabled) {
+                        log.debug("negotiate({}) ignore violating {} KEX 
option={}", this, KexProposalOption.ALGORITHMS, kexOption);
                     }
+                } else {
+                    throw new 
SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, "Illegal KEX 
option negotiated: " + kexOption);
                 }
             }
-        } catch (RuntimeException | Error e) {
+        } catch (IOException | RuntimeException | Error e) {
             signalNegotiationEnd(c2sOptions, s2cOptions, negotiatedGuess, e);
             throw e;
         }
@@ -1803,6 +1902,22 @@ public abstract class AbstractSession extends 
SessionHelper {
         return rekey;
     }
 
+    @Override
+    protected String resolveSessionKexProposal(String hostKeyTypes) throws 
IOException {
+        String proposal = super.resolveSessionKexProposal(hostKeyTypes);
+        KexExtensionHandler extHandler = getKexExtensionHandler();
+        if ((extHandler == null) || 
(!extHandler.isKexExtensionsAvailable(this))) {
+            return proposal;
+        }
+
+        String extType = isServerSession() ? 
KexExtensions.SERVER_KEX_EXTENSION : KexExtensions.CLIENT_KEX_EXTENSION;
+        if (GenericUtils.isEmpty(proposal)) {
+            return extType;
+        } else {
+            return proposal + "," + extType;
+        }
+    }
+
     protected byte[] sendKexInit() throws IOException, 
GeneralSecurityException {
         String resolvedAlgorithms = resolveAvailableSignaturesProposal();
         if (GenericUtils.isEmpty(resolvedAlgorithms)) {
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
index 2846157..1bc8aea 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
@@ -777,17 +777,22 @@ public abstract class SessionHelper extends 
AbstractKexFactoryManager implements
         }
     }
 
+    protected String resolveSessionKexProposal(String hostKeyTypes) throws 
IOException {
+        return NamedResource.getNames(
+            ValidateUtils.checkNotNullAndNotEmpty(getKeyExchangeFactories(), 
"No KEX factories"));
+    }
+
     /**
      * Create our proposal for SSH negotiation
      *
      * @param hostKeyTypes The comma-separated list of supported host key types
      * @return The proposal {@link Map}
+     * @throws IOException If internal problem - e.g., KEX extensions 
negotiation issue
      */
-    protected Map<KexProposalOption, String> createProposal(String 
hostKeyTypes) {
+    protected Map<KexProposalOption, String> createProposal(String 
hostKeyTypes) throws IOException {
         Map<KexProposalOption, String> proposal = new 
EnumMap<>(KexProposalOption.class);
-        proposal.put(KexProposalOption.ALGORITHMS,
-            NamedResource.getNames(
-                
ValidateUtils.checkNotNullAndNotEmpty(getKeyExchangeFactories(), "No KEX 
factories")));
+        String kexProposal = resolveSessionKexProposal(hostKeyTypes);
+        proposal.put(KexProposalOption.ALGORITHMS, kexProposal);
         proposal.put(KexProposalOption.SERVERKEYS, hostKeyTypes);
 
         String ciphers = NamedResource.getNames(
@@ -888,21 +893,6 @@ public abstract class SessionHelper extends 
AbstractKexFactoryManager implements
         listener.sessionNegotiationEnd(this, c2sOptions, s2cOptions, 
negotiatedGuess, null);
     }
 
-    /**
-     * Send a message to put new keys into use.
-     *
-     * @return An {@link IoWriteFuture} that can be used to wait and
-     * check the result of sending the packet
-     * @throws IOException if an error occurs sending the message
-     */
-    protected IoWriteFuture sendNewKeys() throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("sendNewKeys({}) Send SSH_MSG_NEWKEYS", this);
-        }
-        Buffer buffer = createBuffer(SshConstants.SSH_MSG_NEWKEYS, Byte.SIZE);
-        return writePacket(buffer);
-    }
-
     @Override
     public void disconnect(int reason, String msg) throws IOException {
         log.info("Disconnecting({}): {} - {}", this, 
SshConstants.getDisconnectReasonName(reason), msg);
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/hostbased/HostBasedAuthenticator.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/hostbased/HostBasedAuthenticator.java
index 3f069c7..64fe3f5 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/hostbased/HostBasedAuthenticator.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/hostbased/HostBasedAuthenticator.java
@@ -28,7 +28,7 @@ import org.apache.sshd.server.session.ServerSession;
 /**
  * Invoked when &quot;hostbased&quot; authentication is used
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
- * @see <A HREF="https://www.ietf.org/rfc/rfc4252.txt";>RFC 4252 - section 9</A>
+ * @see <A HREF="https://tools.ietf.org/html/rfc4252#section-9";>RFC 4252 - 
section 9</A>
  */
 @FunctionalInterface
 public interface HostBasedAuthenticator {
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/InteractiveChallenge.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/InteractiveChallenge.java
index 94c0be2..a2f1b24 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/InteractiveChallenge.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/InteractiveChallenge.java
@@ -29,7 +29,7 @@ import org.apache.sshd.common.util.buffer.Buffer;
 
 /**
  * Represents a server &quot;challenge&quot; as per
- * <A HREF="https://www.ietf.org/rfc/rfc4256.txt";>RFC-4256</A>
+ * <A HREF="https://tools.ietf.org/html/rfc4256";>RFC-4256</A>
  *
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/KeyboardInteractiveAuthenticator.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/KeyboardInteractiveAuthenticator.java
index 5b78ead..75f5a76 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/KeyboardInteractiveAuthenticator.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/KeyboardInteractiveAuthenticator.java
@@ -25,7 +25,7 @@ import org.apache.sshd.server.session.ServerSession;
 
 /**
  * Provides pluggable authentication using the &quot;keyboard-interactive&quot;
- * method as specified by <A 
HREF="https://www.ietf.org/rfc/rfc4256.txt";>RFC-4256</A>?
+ * method as specified by <A 
HREF="https://tools.ietf.org/html/rfc4256";>RFC-4256</A>?
  *
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/UserAuthKeyboardInteractive.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/UserAuthKeyboardInteractive.java
index e032473..3571837 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/UserAuthKeyboardInteractive.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/UserAuthKeyboardInteractive.java
@@ -31,7 +31,7 @@ import org.apache.sshd.server.auth.AbstractUserAuth;
 import org.apache.sshd.server.session.ServerSession;
 
 /**
- * Issue a &quot;keyboard-interactive&quot; command according to <A 
HREF="https://www.ietf.org/rfc/rfc4256.txt";>RFC4256</A>
+ * Issue a &quot;keyboard-interactive&quot; command according to <A 
HREF="https://tools.ietf.org/html/rfc4256";>RFC4256</A>
  *
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordChangeRequiredException.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordChangeRequiredException.java
index a0fc27d..678dcaa 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordChangeRequiredException.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordChangeRequiredException.java
@@ -23,7 +23,7 @@ package org.apache.sshd.server.auth.password;
  * A special exception that can be thrown by the {@link PasswordAuthenticator}
  * to indicate that the password requires changing or is not string enough
  *
- * @see <A HREF="https://www.ietf.org/rfc/rfc4252.txt";>RFC-4252 section 8 - 
SSH_MSG_USERAUTH_PASSWD_CHANGEREQ</A>
+ * @see <A HREF="https://tools.ietf.org/html/rfc4252#section-8";>RFC-4252 
section 8 - SSH_MSG_USERAUTH_PASSWD_CHANGEREQ</A>
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
 public class PasswordChangeRequiredException extends RuntimeException {
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java
index 2aafa06..79b7bb7 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java
@@ -43,6 +43,8 @@ import org.apache.sshd.common.io.IoWriteFuture;
 import org.apache.sshd.common.kex.KexFactoryManager;
 import org.apache.sshd.common.kex.KexProposalOption;
 import org.apache.sshd.common.kex.KexState;
+import org.apache.sshd.common.kex.extension.KexExtensionHandler;
+import org.apache.sshd.common.kex.extension.KexExtensionHandler.KexPhase;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.session.SessionContext;
@@ -265,10 +267,25 @@ public abstract class AbstractServerSession extends 
AbstractSession implements S
                     "Authentication success signalled though KEX state=" + 
curState);
         }
 
+        KexExtensionHandler extHandler = getKexExtensionHandler();
+        if ((extHandler != null) && extHandler.isKexExtensionsAvailable(this)) 
{
+            extHandler.sendKexExtensions(this, KexPhase.AUTHOK);
+        }
+
         Buffer response = createBuffer(SshConstants.SSH_MSG_USERAUTH_SUCCESS, 
Byte.SIZE);
         IoWriteFuture future;
         IoSession networkSession = getIoSession();
         synchronized (encodeLock) {
+            /*
+             * According to https://tools.ietf.org/html/rfc8308#section-2.4
+             *
+             *      If a server sends SSH_MSG_EXT_INFO, it MAY send it at 
zero, one, or
+             *      both of the following opportunities:
+             *
+             *      ...
+             *
+             *      + Immediately preceding the server's 
SSH_MSG_USERAUTH_SUCCESS
+             */
             Buffer packet = resolveOutputPacket(response);
 
             setUsername(username);
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/common/kex/KexFactoryManagerTest.java 
b/sshd-core/src/test/java/org/apache/sshd/common/kex/KexFactoryManagerTest.java
index 8242a62..0832ab4 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/common/kex/KexFactoryManagerTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/common/kex/KexFactoryManagerTest.java
@@ -27,6 +27,7 @@ import org.apache.sshd.common.cipher.BuiltinCiphers;
 import org.apache.sshd.common.cipher.Cipher;
 import org.apache.sshd.common.compression.BuiltinCompressions;
 import org.apache.sshd.common.compression.Compression;
+import org.apache.sshd.common.kex.extension.KexExtensionHandler;
 import org.apache.sshd.common.mac.BuiltinMacs;
 import org.apache.sshd.common.mac.Mac;
 import org.apache.sshd.common.signature.BuiltinSignatures;
@@ -122,6 +123,7 @@ public class KexFactoryManagerTest extends BaseTestSupport {
         private List<NamedFactory<Cipher>> ciphers;
         private List<NamedFactory<Mac>> macs;
         private List<NamedFactory<Signature>> signatures;
+        private KexExtensionHandler kexExtensionHandler;
 
         TestKexFactoryManager() {
             super();
@@ -176,5 +178,15 @@ public class KexFactoryManagerTest extends BaseTestSupport 
{
         public void setMacFactories(List<NamedFactory<Mac>> macFactories) {
             macs = macFactories;
         }
+
+        @Override
+        public KexExtensionHandler getKexExtensionHandler() {
+            return kexExtensionHandler;
+        }
+
+        @Override
+        public void setKexExtensionHandler(KexExtensionHandler handler) {
+            this.kexExtensionHandler = handler;
+        }
     }
 }
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java
new file mode 100644
index 0000000..cd08eba
--- /dev/null
+++ 
b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.common.kex.extension;
+
+import java.io.IOException;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sshd.common.kex.extension.parser.DelayCompression;
+import 
org.apache.sshd.common.kex.extension.parser.DelayedCompressionAlgorithms;
+import org.apache.sshd.common.kex.extension.parser.Elevation;
+import org.apache.sshd.common.kex.extension.parser.NoFlowControl;
+import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.util.test.JUnitTestSupport;
+import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runners.MethodSorters;
+import org.mockito.Mockito;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Category({ NoIoTestCase.class })
+public class KexExtensionHandlerTest extends JUnitTestSupport {
+    public KexExtensionHandlerTest() {
+        super();
+    }
+
+    @Test
+    public void testEncodeDecodeExtensionMessage() throws IOException {
+        List<Map.Entry<String, ?>> expected = Arrays.asList(
+            new SimpleImmutableEntry<>(DelayCompression.NAME,
+                new DelayedCompressionAlgorithms()
+                    .withClient2Server(
+                        Arrays.asList(getClass().getSimpleName(), 
getCurrentTestName()))
+                    .withServer2Client(
+                        Arrays.asList(getClass().getPackage().getName(), 
getCurrentTestName()))),
+            new SimpleImmutableEntry<>(ServerSignatureAlgorithms.NAME,
+                Arrays.asList(getClass().getPackage().getName(), 
getClass().getSimpleName(), getCurrentTestName())),
+            new SimpleImmutableEntry<>(NoFlowControl.NAME, 
getCurrentTestName()),
+            new SimpleImmutableEntry<>(Elevation.NAME, getCurrentTestName()));
+        Buffer buffer = new ByteArrayBuffer();
+        KexExtensions.putExtensions(expected, buffer);
+
+        List<Map.Entry<String, ?>> actual = new ArrayList<>(expected.size());
+        KexExtensionHandler handler = new KexExtensionHandler() {
+            @Override
+            public boolean handleKexExtensionRequest(Session session, int 
index, int count, String name, byte[] data)
+                    throws IOException {
+                KexExtensionParser<?> parser = 
KexExtensions.getRegisteredExtensionParser(name);
+                assertNotNull("No parser found for extension=" + name, parser);
+
+                Object value = parser.parseExtension(data);
+                assertNotNull("No value extracted for extension=" + name, 
value);
+                actual.add(new SimpleImmutableEntry<>(name, value));
+                return true;
+            }
+        };
+        Session session = Mockito.mock(Session.class);
+        handler.handleKexExtensionsMessage(session, buffer);
+
+        assertEquals("Mismatched recovered extensions count", expected.size(), 
actual.size());
+        for (int index = 0; index < actual.size(); index++) {
+            Map.Entry<String, ?> expEntry = expected.get(index);
+            String name = expEntry.getKey();
+            Map.Entry<String, ?> actEntry = actual.get(index);
+            assertEquals("Mismatched extension name at index=" + index, name, 
actEntry.getKey());
+
+            Object expValue = expEntry.getValue();
+            Object actValue = actEntry.getValue();
+            if (expValue instanceof List<?>) {
+                assertListEquals(name, (List<?>) expValue, (List<?>) actValue);
+            } else {
+                assertEquals("Mismatched values for extension=" + name, 
expValue, actValue);
+            }
+        }
+    }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java 
b/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java
index 224bfa2..73cfc25 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java
@@ -428,7 +428,7 @@ public class ServerTest extends BaseTestSupport {
             protected ClientSessionImpl doCreateSession(IoSession ioSession) 
throws Exception {
                 return new ClientSessionImpl(getClient(), ioSession) {
                     @Override
-                    protected Map<KexProposalOption, String> 
createProposal(String hostKeyTypes) {
+                    protected Map<KexProposalOption, String> 
createProposal(String hostKeyTypes) throws IOException {
                         Map<KexProposalOption, String> proposal = 
super.createProposal(hostKeyTypes);
                         proposal.put(KexProposalOption.S2CLANG, "en-US");
                         proposal.put(KexProposalOption.C2SLANG, "en-US");
@@ -491,7 +491,7 @@ public class ServerTest extends BaseTestSupport {
             protected ServerSessionImpl doCreateSession(IoSession ioSession) 
throws Exception {
                 return new ServerSessionImpl(getServer(), ioSession) {
                     @Override
-                    protected Map<KexProposalOption, String> 
createProposal(String hostKeyTypes) {
+                    protected Map<KexProposalOption, String> 
createProposal(String hostKeyTypes) throws IOException {
                         Map<KexProposalOption, String> proposal = 
super.createProposal(hostKeyTypes);
                         proposal.put(KexProposalOption.C2SCOMP, 
getCurrentTestName());
                         proposal.put(KexProposalOption.S2CCOMP, 
getCurrentTestName());
@@ -507,7 +507,7 @@ public class ServerTest extends BaseTestSupport {
             protected ClientSessionImpl doCreateSession(IoSession ioSession) 
throws Exception {
                 return new ClientSessionImpl(getClient(), ioSession) {
                     @Override
-                    protected Map<KexProposalOption, String> 
createProposal(String hostKeyTypes) {
+                    protected Map<KexProposalOption, String> 
createProposal(String hostKeyTypes) throws IOException {
                         Map<KexProposalOption, String> proposal = 
super.createProposal(hostKeyTypes);
                         proposal.put(KexProposalOption.C2SCOMP, 
getCurrentTestName());
                         proposal.put(KexProposalOption.S2CCOMP, 
getCurrentTestName());

Reply via email to