Implement the "sntrup761x25519-sha512@openssh.com" KEX method

This uses a post-quantum key encapsulation method (KEM) to make key
exchange future-proof against quantum attacks. It is to be preferred
over curve25519-sha256 "when the extra communication size and
computational requirements are acceptable."[1] (curve25519-sha256
exchanged 32 bytes where sntrup761x25519-sha512 exchanges 1190 or 1071
bytes.)

This KEX method changes the encoding of the key from 'mpint' to
'string'. To make the handling of the K value more uniform, change
it to 'string' everywhere, and convert mpints with the high bit set
explicitly by prepending a zero byte.

Separate the digest from MontgomeryCurve; handle combining curves and
hashes (and KEMs) in the BuiltinDHFactories instead.

In the BaseBuilder, add "sntrup761x25519-sha512@openssh.com" as first
(i.e., preferred) KEX algorithm.

[1] https://www.ietf.org/archive/id/draft-josefsson-ntruprime-ssh-02.html
diff --git a/CHANGES.md b/CHANGES.md
index 05724a4..7da6113 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -47,6 +47,13 @@
 
 ## New Features
 
+* The key exchange method sntrup761x25519-sha512@openssh.com is now available if the Bouncy Castle library is available.
+
+This uses a post-quantum key encapsulation method (KEM) to make key exchange future-proof against quantum attacks.
+More information can be found in IETF Memo [Secure Shell (SSH) Key Exchange Method Using Hybrid Streamlined
+NTRU Prime sntrup761 and X25519 with SHA-512: sntrup761x25519-sha512](https://www.ietf.org/archive/id/draft-josefsson-ntruprime-ssh-02.html).
+
+
 ## Behavioral changes and enhancements
 
 * [GH-468](https://github.com/apache/mina-sshd/issues/468) SFTP: validate length of data received: must not be more than requested
diff --git a/docs/standards.md b/docs/standards.md
index c2d8f28..9998f66 100644
--- a/docs/standards.md
+++ b/docs/standards.md
@@ -29,6 +29,7 @@
     above mentioned hooks for [RFC 8308](https://tools.ietf.org/html/rfc8308).
 * [RFC 8731 - Secure Shell (SSH) Key Exchange Method Using Curve25519 and Curve448](https://tools.ietf.org/html/rfc8731)
 * [Key Exchange (KEX) Method Updates and Recommendations for Secure Shell](https://tools.ietf.org/html/draft-ietf-curdle-ssh-kex-sha2-03)
+* [Secure Shell (SSH) Key Exchange Method Using Hybrid Streamlined NTRU Prime sntrup761 and X25519 with SHA-512: sntrup761x25519-sha512](https://www.ietf.org/archive/id/draft-josefsson-ntruprime-ssh-02.html)
 
 ### OpenSSH
 
@@ -95,8 +96,10 @@
 
 * diffie-hellman-group1-sha1, diffie-hellman-group-exchange-sha256, diffie-hellman-group14-sha1, diffie-hellman-group14-sha256
 , diffie-hellman-group15-sha512, diffie-hellman-group16-sha512, diffie-hellman-group17-sha512, diffie-hellman-group18-sha512
-, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, curve25519-sha256, curve25519-sha256@libssh.org, curve448-sha512
+, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, curve25519-sha256, curve25519-sha256@libssh.org, curve448-sha512,
+sntrup761x25519-sha512@openssh.com
     * On Java versions before Java 11, [Bouncy Castle](./dependencies.md#bouncy-castle) is required for curve25519-sha256, curve25519-sha256@libssh.org, or curve448-sha512.
+    * [Bouncy Castle](./dependencies.md#bouncy-castle) is required for sntrup761x25519-sha512@openssh.com.
 
 ### Compressions
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java
index 294f3c7..9235ffb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java
@@ -21,6 +21,7 @@
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.security.PublicKey;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Objects;
 
@@ -30,11 +31,14 @@
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.digest.Digest;
 import org.apache.sshd.common.kex.AbstractDH;
 import org.apache.sshd.common.kex.DHFactory;
 import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.kex.KeyEncapsulationMethod;
 import org.apache.sshd.common.kex.KeyExchange;
 import org.apache.sshd.common.kex.KeyExchangeFactory;
+import org.apache.sshd.common.kex.XDH;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.signature.Signature;
@@ -55,6 +59,8 @@
     protected final DHFactory factory;
     protected AbstractDH dh;
 
+    private KeyEncapsulationMethod.Client kemClient;
+
     protected DHGClient(DHFactory factory, Session session) {
         super(session);
 
@@ -95,7 +101,20 @@
         hash = dh.getHash();
         hash.init();
 
-        byte[] e = updateE(dh.getE());
+        KeyEncapsulationMethod kem = dh.getKeyEncapsulation();
+        byte[] e;
+        if (kem == null) {
+            e = updateE(dh.getE());
+        } else {
+            kemClient = kem.getClient();
+            kemClient.init();
+            e = kemClient.getPublicKey();
+            byte[] dhE = dh.getE();
+            int l = e.length;
+            e = Arrays.copyOf(e, l + dhE.length);
+            System.arraycopy(dhE, 0, e, l, dhE.length);
+            e = updateE(e);
+        }
 
         Session s = getSession();
         if (log.isDebugEnabled()) {
@@ -129,8 +148,32 @@
         byte[] f = updateF(buffer);
         byte[] sig = buffer.getBytes();
 
-        dh.setF(f);
-        k = dh.getK();
+        if (kemClient == null) {
+            dh.setF(f);
+            k = normalize(dh.getK());
+        } else {
+            try {
+                int l = kemClient.getEncapsulationLength();
+                if (dh instanceof XDH) {
+                    if (f.length != l + ((XDH) dh).getKeySize()) {
+                        throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
+                                "Wrong F length (should be 1071 bytes): " + f.length);
+                    }
+                } else {
+                    throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
+                            "Key encapsulation only supported for XDH");
+                }
+                dh.setF(Arrays.copyOfRange(f, l, f.length));
+                Digest keyHash = dh.getHash();
+                keyHash.init();
+                keyHash.update(kemClient.extractSecret(Arrays.copyOf(f, l)));
+                keyHash.update(dh.getK());
+                k = keyHash.digest();
+            } catch (IllegalArgumentException ex) {
+                throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
+                        "Key encapsulation error: " + ex.getMessage());
+            }
+        }
 
         buffer = new ByteArrayBuffer(k_s);
         PublicKey serverKey = buffer.getRawPublicKey();
@@ -167,7 +210,7 @@
         buffer.putBytes(k_s);
         dh.putE(buffer, getE());
         dh.putF(buffer, f);
-        buffer.putMPInt(k);
+        buffer.putBytes(k);
         hash.update(buffer.array(), 0, buffer.available());
         h = hash.digest();
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGEXClient.java b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGEXClient.java
index 3452bbc..ac9eecf 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGEXClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/kex/DHGEXClient.java
@@ -202,7 +202,7 @@
             validateFValue();
 
             dh.setF(f);
-            k = dh.getK();
+            k = normalize(dh.getK());
 
             buffer = new ByteArrayBuffer(k_s);
             PublicKey serverKey = buffer.getRawPublicKey();
@@ -226,7 +226,7 @@
             buffer.putMPInt(g);
             buffer.putMPInt(getE());
             buffer.putMPInt(f);
-            buffer.putMPInt(k);
+            buffer.putBytes(k);
             hash.update(buffer.array(), 0, buffer.available());
             h = hash.digest();
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java b/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java
index 3386e80..0c2ecc9 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java
@@ -87,6 +87,7 @@
      */
     public static final List<BuiltinDHFactories> DEFAULT_KEX_PREFERENCE = Collections.unmodifiableList(
             Arrays.asList(
+                    BuiltinDHFactories.sntrup761x25519,
                     BuiltinDHFactories.curve25519,
                     BuiltinDHFactories.curve25519_libssh,
                     BuiltinDHFactories.curve448,
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractDH.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractDH.java
index 5480b44..b9de78f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractDH.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractDH.java
@@ -118,6 +118,10 @@
 
     public abstract Digest getHash() throws Exception;
 
+    public KeyEncapsulationMethod getKeyEncapsulation() {
+        return null;
+    }
+
     @Override
     public String toString() {
         return getClass().getSimpleName()
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinDHFactories.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinDHFactories.java
index 3d7e854..c0c3c5a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinDHFactories.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinDHFactories.java
@@ -36,6 +36,7 @@
 import org.apache.sshd.common.cipher.ECCurves;
 import org.apache.sshd.common.config.NamedResourceListParseResult;
 import org.apache.sshd.common.digest.BuiltinDigests;
+import org.apache.sshd.common.digest.Digest;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.security.SecurityUtils;
@@ -252,12 +253,19 @@
             if (!GenericUtils.isEmpty(params)) {
                 throw new IllegalArgumentException("No accepted parameters for " + getName());
             }
-            return new XDH(MontgomeryCurve.x25519);
+            return new XDH(MontgomeryCurve.x25519) {
+
+                @Override
+                public Digest getHash() throws Exception {
+                    return BuiltinDigests.sha256.create();
+                }
+
+            };
         }
 
         @Override
         public boolean isSupported() {
-            return MontgomeryCurve.x25519.isSupported();
+            return MontgomeryCurve.x25519.isSupported() && BuiltinDigests.sha256.isSupported();
         }
     },
     curve25519_libssh(Constants.CURVE25519_SHA256_LIBSSH) {
@@ -266,12 +274,19 @@
             if (!GenericUtils.isEmpty(params)) {
                 throw new IllegalArgumentException("No accepted parameters for " + getName());
             }
-            return new XDH(MontgomeryCurve.x25519);
+            return new XDH(MontgomeryCurve.x25519) {
+
+                @Override
+                public Digest getHash() throws Exception {
+                    return BuiltinDigests.sha256.create();
+                }
+
+            };
         }
 
         @Override
         public boolean isSupported() {
-            return MontgomeryCurve.x25519.isSupported();
+            return MontgomeryCurve.x25519.isSupported() && BuiltinDigests.sha256.isSupported();
         }
     },
     /**
@@ -283,12 +298,48 @@
             if (!GenericUtils.isEmpty(params)) {
                 throw new IllegalArgumentException("No accepted parameters for " + getName());
             }
-            return new XDH(MontgomeryCurve.x448);
+            return new XDH(MontgomeryCurve.x448) {
+
+                @Override
+                public Digest getHash() throws Exception {
+                    return BuiltinDigests.sha512.create();
+                }
+            };
         }
 
         @Override
         public boolean isSupported() {
-            return MontgomeryCurve.x448.isSupported();
+            return MontgomeryCurve.x448.isSupported() && BuiltinDigests.sha512.isSupported();
+        }
+    },
+    /**
+     * @see <a href=
+     *      "https://www.ietf.org/archive/id/draft-josefsson-ntruprime-ssh-02.html">draft-josefsson-ntruprime-ssh-02.html</a>
+     */
+    sntrup761x25519(Constants.SNTRUP761_25519_SHA512) {
+        @Override
+        public XDH create(Object... params) throws Exception {
+            if (!GenericUtils.isEmpty(params)) {
+                throw new IllegalArgumentException("No accepted parameters for " + getName());
+            }
+            return new XDH(MontgomeryCurve.x25519) {
+
+                @Override
+                public KeyEncapsulationMethod getKeyEncapsulation() {
+                    return BuiltinKEM.sntrup761;
+                }
+
+                @Override
+                public Digest getHash() throws Exception {
+                    return BuiltinDigests.sha512.create();
+                }
+            };
+        }
+
+        @Override
+        public boolean isSupported() {
+            return MontgomeryCurve.x25519.isSupported() && BuiltinDigests.sha512.isSupported()
+                    && BuiltinKEM.sntrup761.isSupported();
         }
     };
 
@@ -468,6 +519,7 @@
         public static final String CURVE25519_SHA256 = "curve25519-sha256";
         public static final String CURVE25519_SHA256_LIBSSH = "curve25519-sha256@libssh.org";
         public static final String CURVE448_SHA512 = "curve448-sha512";
+        public static final String SNTRUP761_25519_SHA512 = "sntrup761x25519-sha512@openssh.com";
 
         private Constants() {
             throw new UnsupportedOperationException("No instance allowed");
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinKEM.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinKEM.java
new file mode 100644
index 0000000..33997f5
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinKEM.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.OptionalFeature;
+
+/**
+ * All built in key encapsulation methods (KEM).
+ */
+public enum BuiltinKEM implements KeyEncapsulationMethod, NamedResource, OptionalFeature {
+
+    sntrup761("sntrup761") {
+
+        @Override
+        public Client getClient() {
+            return new SNTRUP761.Client();
+        }
+
+        @Override
+        public Server getServer() {
+            return new SNTRUP761.Server();
+        }
+
+        @Override
+        public boolean isSupported() {
+            return SNTRUP761.isSupported();
+        }
+
+    };
+
+    private String name;
+
+    BuiltinKEM(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/KeyEncapsulationMethod.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/KeyEncapsulationMethod.java
new file mode 100644
index 0000000..a836127
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/KeyEncapsulationMethod.java
@@ -0,0 +1,94 @@
+/*
+ * 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;
+
+/**
+ * General interface for key encapsulation methods (KEM).
+ */
+public interface KeyEncapsulationMethod {
+
+    /**
+     * Client-side KEM operations.
+     */
+    interface Client {
+
+        /**
+         * Initializes the KEM and generates a new key pair.
+         */
+        void init();
+
+        /**
+         * Retrieves the KEM public key.
+         */
+        byte[] getPublicKey();
+
+        /**
+         * Extracts the secret from an encapsulation ciphertext.
+         *
+         * @param  encapsulated             ciphertext to process.
+         *
+         * @throws IllegalArgumentException if {@code encapsulated} doesn't have the expected length
+         * @throws NullPointerException     if {@code encapsulated == null}
+         */
+        byte[] extractSecret(byte[] encapsulated);
+
+        /**
+         * Retrieves the required encapsulation length in bytes.
+         *
+         * @return the length required for a valid encapsulation ciphertext
+         */
+        int getEncapsulationLength();
+    }
+
+    /**
+     * Server-side KEM operations.
+     */
+    interface Server {
+
+        /**
+         * Initializes the KEM with a public key received from a client and prepares an encapsulated secret.
+         *
+         * @param  publicKey                data received from the client, expected to contain the public key at the
+         *                                  start
+         * @return                          the remaining bytes of {@code publicKey} after the public key
+         *
+         * @throws IllegalArgumentException if {@code publicKey} does not have enough bytes for a valid public key
+         * @throws NullPointerException     if {@code publicKey == null}
+         */
+        byte[] init(byte[] publicKey);
+
+        /**
+         * Retrieves the secret.
+         *
+         * @return the secret, not encapsulated
+         */
+        byte[] getSecret();
+
+        /**
+         * Retrieves the encapsulation of the secret.
+         *
+         * @return the encapsulation of the secret that may be sent to the client
+         */
+        byte[] getEncapsulation();
+    }
+
+    Client getClient();
+
+    Server getServer();
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/MontgomeryCurve.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/MontgomeryCurve.java
index 1b7684e..3f6904f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/kex/MontgomeryCurve.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/MontgomeryCurve.java
@@ -31,9 +31,6 @@
 import javax.crypto.KeyAgreement;
 
 import org.apache.sshd.common.OptionalFeature;
-import org.apache.sshd.common.digest.BuiltinDigests;
-import org.apache.sshd.common.digest.Digest;
-import org.apache.sshd.common.digest.DigestFactory;
 import org.apache.sshd.common.keyprovider.KeySizeIndicator;
 import org.apache.sshd.common.util.security.SecurityUtils;
 
@@ -92,40 +89,38 @@
     /**
      * X25519 uses Curve25519 and SHA-256 with a 32-byte key size.
      */
-    x25519("X25519", 32, BuiltinDigests.sha256,
+    x25519("X25519", 32,
            new byte[] { 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00 }),
 
     /**
      * X448 uses Curve448 and SHA-512 with a 56-byte key size.
      */
-    x448("X448", 56, BuiltinDigests.sha512,
+    x448("X448", 56,
          new byte[] { 0x30, 0x42, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6f, 0x03, 0x39, 0x00 });
 
     private final String algorithm;
     private final int keySize;
     private final boolean supported;
-    private final DigestFactory digestFactory;
     private final KeyPairGenerator keyPairGenerator;
     private final KeyFactory keyFactory;
     private final byte[] encodedPublicKeyPrefix;
 
-    MontgomeryCurve(String algorithm, int keySize, DigestFactory digestFactory, byte[] encodedPublicKeyPrefix) {
+    MontgomeryCurve(String algorithm, int keySize, byte[] encodedPublicKeyPrefix) {
         this.algorithm = algorithm;
         this.keySize = keySize;
-        this.digestFactory = digestFactory;
         this.encodedPublicKeyPrefix = encodedPublicKeyPrefix;
-        boolean supported;
+        boolean isSupported;
         KeyPairGenerator generator = null;
         KeyFactory factory = null;
         try {
             SecurityUtils.getKeyAgreement(algorithm);
             generator = SecurityUtils.getKeyPairGenerator(algorithm);
             factory = SecurityUtils.getKeyFactory(algorithm);
-            supported = true;
+            isSupported = true;
         } catch (GeneralSecurityException ignored) {
-            supported = false;
+            isSupported = false;
         }
-        this.supported = supported && digestFactory.isSupported();
+        this.supported = isSupported;
         keyPairGenerator = generator;
         keyFactory = factory;
     }
@@ -148,10 +143,6 @@
         return SecurityUtils.getKeyAgreement(algorithm);
     }
 
-    public Digest createDigest() {
-        return digestFactory.create();
-    }
-
     public KeyPair generateKeyPair() {
         synchronized (this) {
             return keyPairGenerator.generateKeyPair();
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/SNTRUP761.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/SNTRUP761.java
new file mode 100644
index 0000000..81df234
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/SNTRUP761.java
@@ -0,0 +1,124 @@
+/*
+ * 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;
+
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+import org.bouncycastle.crypto.SecretWithEncapsulation;
+import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimeKEMExtractor;
+import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimeKEMGenerator;
+import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimeKeyGenerationParameters;
+import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimeKeyPairGenerator;
+import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimeParameters;
+import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimePrivateKeyParameters;
+import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimePublicKeyParameters;
+
+/**
+ * A Bouncy Castle implementation of the sntrup761 key encapsulation method (KEM).
+ */
+final class SNTRUP761 {
+
+    private SNTRUP761() {
+        // No instantiation
+    }
+
+    static boolean isSupported() {
+        if (!SecurityUtils.isBouncyCastleRegistered()) {
+            return false;
+        }
+        try {
+            return SNTRUPrimeParameters.sntrup761.getSessionKeySize() == 256; // BC < 1.78 had only 128
+        } catch (Throwable e) {
+            return false;
+        }
+    }
+
+    static class Client implements KeyEncapsulationMethod.Client {
+
+        private SNTRUPrimeKEMExtractor extractor;
+        private SNTRUPrimePublicKeyParameters publicKey;
+
+        Client() {
+            super();
+        }
+
+        @Override
+        public void init() {
+            SNTRUPrimeKeyPairGenerator gen = new SNTRUPrimeKeyPairGenerator();
+            gen.init(new SNTRUPrimeKeyGenerationParameters(new SecureRandom(), SNTRUPrimeParameters.sntrup761));
+            AsymmetricCipherKeyPair pair = gen.generateKeyPair();
+            extractor = new SNTRUPrimeKEMExtractor((SNTRUPrimePrivateKeyParameters) pair.getPrivate());
+            publicKey = (SNTRUPrimePublicKeyParameters) pair.getPublic();
+        }
+
+        @Override
+        public byte[] getPublicKey() {
+            return publicKey.getEncoded();
+        }
+
+        @Override
+        public byte[] extractSecret(byte[] encapsulated) {
+            if (encapsulated.length != extractor.getEncapsulationLength()) {
+                throw new IllegalArgumentException("KEM encpsulation has wrong length: " + encapsulated.length);
+            }
+            return extractor.extractSecret(encapsulated);
+        }
+
+        @Override
+        public int getEncapsulationLength() {
+            return extractor.getEncapsulationLength();
+        }
+    }
+
+    static class Server implements KeyEncapsulationMethod.Server {
+
+        private SecretWithEncapsulation value;
+
+        Server() {
+            super();
+        }
+
+        @Override
+        public byte[] init(byte[] publicKey) {
+            int pkBytes = SNTRUPrimeParameters.sntrup761.getPublicKeyBytes();
+            if (publicKey.length < pkBytes) {
+                throw new IllegalArgumentException("KEM public key too short: " + publicKey.length);
+            }
+            byte[] pk = Arrays.copyOf(publicKey, pkBytes);
+            SNTRUPrimeKEMGenerator kemGenerator = new SNTRUPrimeKEMGenerator(new SecureRandom());
+            SNTRUPrimePublicKeyParameters params = new SNTRUPrimePublicKeyParameters(SNTRUPrimeParameters.sntrup761, pk);
+            value = kemGenerator.generateEncapsulated(params);
+            return Arrays.copyOfRange(publicKey, pkBytes, publicKey.length);
+        }
+
+        @Override
+        public byte[] getSecret() {
+            return value.getSecret();
+        }
+
+        @Override
+        public byte[] getEncapsulation() {
+            return value.getEncapsulation();
+        }
+
+    }
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/XDH.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/XDH.java
index 5d3fcab..f321251 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/kex/XDH.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/XDH.java
@@ -22,7 +22,6 @@
 import java.security.KeyPair;
 import java.util.Objects;
 
-import org.apache.sshd.common.digest.Digest;
 import org.apache.sshd.common.util.buffer.Buffer;
 
 /**
@@ -30,7 +29,7 @@
  *
  * @see <a href="https://www.rfc-editor.org/info/rfc8731">RFC 8731</a>
  */
-public class XDH extends AbstractDH {
+public abstract class XDH extends AbstractDH {
 
     protected MontgomeryCurve curve;
     protected byte[] f;
@@ -40,6 +39,10 @@
         myKeyAgree = curve.createKeyAgreement();
     }
 
+    public int getKeySize() {
+        return curve.getKeySize();
+    }
+
     @Override
     protected byte[] calculateE() throws Exception {
         KeyPair keyPair = curve.generateKeyPair();
@@ -76,9 +79,4 @@
         myKeyAgree.doPhase(curve.decode(f), true);
         return stripLeadingZeroes(myKeyAgree.generateSecret());
     }
-
-    @Override
-    public Digest getHash() throws Exception {
-        return curve.createDigest();
-    }
 }
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/dh/AbstractDHKeyExchange.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/dh/AbstractDHKeyExchange.java
index 746484b..2f5e019 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/kex/dh/AbstractDHKeyExchange.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/dh/AbstractDHKeyExchange.java
@@ -27,6 +27,7 @@
 import org.apache.sshd.common.digest.Digest;
 import org.apache.sshd.common.kex.KeyExchange;
 import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.NumberUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.BufferUtils;
@@ -161,4 +162,14 @@
     public String toString() {
         return getClass().getSimpleName() + "[" + getName() + "]";
     }
+
+    protected byte[] normalize(byte[] mpInt) {
+        if (!NumberUtils.isEmpty(mpInt) && (mpInt[0] & 0x80) != 0) {
+            byte[] result = new byte[mpInt.length + 1];
+            result[0] = 0;
+            System.arraycopy(mpInt, 0, result, 1, mpInt.length);
+            return result;
+        }
+        return mpInt;
+    }
 }
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 b05a3ab..921e287 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
@@ -1873,7 +1873,7 @@
         }
 
         Buffer buffer = new ByteArrayBuffer();
-        buffer.putMPInt(k);
+        buffer.putBytes(k);
         buffer.putRawBytes(h);
         buffer.putByte((byte) 0x41);
         buffer.putRawBytes(sessionId);
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGEXServer.java b/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGEXServer.java
index 6ba71ca..7b6c6e2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGEXServer.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGEXServer.java
@@ -197,7 +197,7 @@
 
             dh.setF(e);
 
-            k = dh.getK();
+            k = normalize(dh.getK());
 
             KeyPair kp = Objects.requireNonNull(session.getHostKey(), "No server key pair available");
             String algo = session.getNegotiatedKexParameter(KexProposalOption.SERVERKEYS);
@@ -231,7 +231,7 @@
             buffer.putMPInt(e);
             byte[] f = getF();
             buffer.putMPInt(f);
-            buffer.putMPInt(k);
+            buffer.putBytes(k);
 
             hash.update(buffer.array(), 0, buffer.available());
             h = hash.digest();
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGServer.java b/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGServer.java
index 3d1e02b..2d90fcc 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGServer.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/kex/DHGServer.java
@@ -19,16 +19,20 @@
 package org.apache.sshd.server.kex;
 
 import java.security.KeyPair;
+import java.util.Arrays;
 import java.util.Objects;
 
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.digest.Digest;
 import org.apache.sshd.common.kex.AbstractDH;
 import org.apache.sshd.common.kex.DHFactory;
 import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.kex.KeyEncapsulationMethod;
 import org.apache.sshd.common.kex.KeyExchange;
 import org.apache.sshd.common.kex.KeyExchangeFactory;
+import org.apache.sshd.common.kex.XDH;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.signature.Signature;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -99,8 +103,41 @@
         }
 
         byte[] e = updateE(buffer);
-        dh.setF(e);
-        k = dh.getK();
+        KeyEncapsulationMethod kem = dh.getKeyEncapsulation();
+        if (kem == null) {
+            dh.setF(e);
+            k = normalize(dh.getK());
+        } else {
+            try {
+                KeyEncapsulationMethod.Server kemServer = kem.getServer();
+
+                byte[] f = kemServer.init(e);
+                if (dh instanceof XDH) {
+                    if (f.length != ((XDH) dh).getKeySize()) {
+                        throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
+                                "Wrong E length (should be 1190 bytes): " + e.length);
+                    }
+                } else {
+                    throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
+                            "Key encapsulation only supported for XDH");
+                }
+                dh.setF(f);
+                byte[] dhK = dh.getK();
+                Digest keyHash = dh.getHash();
+                keyHash.init();
+                keyHash.update(kemServer.getSecret());
+                keyHash.update(dhK);
+                k = keyHash.digest();
+                byte[] newF = kemServer.getEncapsulation();
+                int l = newF.length;
+                newF = Arrays.copyOf(newF, l + dh.getE().length);
+                System.arraycopy(dh.getE(), 0, newF, l, dh.getE().length);
+                setF(newF);
+            } catch (IllegalArgumentException ex) {
+                throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
+                        "Key encapsulation error: " + ex.getMessage());
+            }
+        }
 
         KeyPair kp = Objects.requireNonNull(session.getHostKey(), "No server key pair available");
         String algo = session.getNegotiatedKexParameter(KexProposalOption.SERVERKEYS);
@@ -123,7 +160,7 @@
         dh.putE(buffer, e);
         byte[] f = getF();
         dh.putF(buffer, f);
-        buffer.putMPInt(k);
+        buffer.putBytes(k);
 
         hash.update(buffer.array(), 0, buffer.available());
         h = hash.digest();