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();