Skip to content

Commit

Permalink
GH-606: ML-KEM key exchange implementation using Bouncy Castle
Browse files Browse the repository at this point in the history
Refactor the KEM-based KEX paths a little bit; provide the ML-KEMs, and
add the DH factories combining the ML-KEMs with the base curves and
hashes.

KexTest tests that the new key exchanges do work between an Apache MINA
sshd client and server. Add an integration test that verifies that the
new ML-KEM kex works against an OpenSSH 9.9 server (it only has
mlkem768x25519, not the other two variants using ECDH nistp256/384, so
we can't test those).
  • Loading branch information
tomaswolf committed Nov 4, 2024
1 parent 00fb9b6 commit 38bb2c6
Show file tree
Hide file tree
Showing 17 changed files with 531 additions and 49 deletions.
4 changes: 3 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

# [Version 2.13.1 to 2.13.2](./docs/changes/2.13.2.md)

# [Version 2.13.1 to 2.14.0](./docs/changes/2.14.0.md)
# [Version 2.13.2 to 2.14.0](./docs/changes/2.14.0.md)

# Planned for next version

Expand All @@ -44,6 +44,8 @@

## New Features

* [GH-606](https://github.com/apache/mina-sshd/issues/606) Support ML-KEM PQC key exchange

## Potential compatibility issues

## Major Code Re-factoring
Expand Down
17 changes: 10 additions & 7 deletions docs/standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
* [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)
* [PQ/T Hybrid Key Exchange in SSH](https://datatracker.ietf.org/doc/html/draft-kampanakis-curdle-ssh-pq-ke-04)

### OpenSSH

Expand Down Expand Up @@ -96,10 +97,11 @@ [email protected], [email protected], [email protected], 3

* 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, [email protected], curve448-sha512,
[email protected]
* On Java versions before Java 11, [Bouncy Castle](./dependencies.md#bouncy-castle) is required for curve25519-sha256, [email protected], or curve448-sha512.
* [Bouncy Castle](./dependencies.md#bouncy-castle) is required for [email protected].
, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, curve25519-sha256, curve25519-sha256@<!-- -->libssh.org, curve448-sha512
* On Java versions before Java 11, [Bouncy Castle](./dependencies.md#bouncy-castle) is required for curve25519-sha256, curve25519-sha256@<!-- -->libssh.org, or curve448-sha512.

* If [Bouncy Castle](./dependencies.md#bouncy-castle) is present, the following post-quantum cryptography (PQC) hybrid key exchanges are also supported: sntrup761x25519-sha512, sntrup761x25519-sha512@<!-- -->openssh.com, mlkem768x25519-sha256, mlkem768nistp256-sha256, and
mlkem1024nistp384-sha384.

### Compressions

Expand All @@ -108,9 +110,10 @@ [email protected]
### Signatures/Keys

* ssh-dss, ssh-rsa, rsa-sha2-256, rsa-sha2-512, nistp256, nistp384, nistp521
, ssh-ed25519 (requires `eddsa` optional module), [email protected], [email protected]
, [email protected], [email protected], [email protected]
, [email protected], [email protected], [email protected]
, ssh-ed25519 (requires `eddsa` optional module), [email protected], sk-ssh-ed25519@<!-- -->openssh.com
, ssh-rsa-cert-v01@<!-- -->openssh.com, ssh-dss-cert-v01<!-- -->@openssh.com, ssh-ed25519-cert-v01@<!-- -->openssh.com
, ecdsa-sha2-nistp256-cert-v01@<!-- -->openssh.com, ecdsa-sha2-nistp384-cert-v01<!-- -->@openssh.com
, ecdsa-sha2-nistp521-cert-v01<!-- -->@openssh.com

**Note:** The above list contains all the supported security settings in the code. However, in accordance with the latest recommendations
the default client/server setup includes only the security settings that are currently considered safe to use. Users who wish to include
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@
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.CurveSizeIndicator;
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;
Expand Down Expand Up @@ -154,14 +154,15 @@ public boolean next(int cmd, Buffer buffer) throws Exception {
} else {
try {
int l = kemClient.getEncapsulationLength();
if (dh instanceof XDH) {
if (f.length != l + ((XDH) dh).getKeySize()) {
if (dh instanceof CurveSizeIndicator) {
int expectedLength = l + ((CurveSizeIndicator) dh).getByteLength();
if (f.length != expectedLength) {
throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
"Wrong F length (should be 1071 bytes): " + f.length);
"Wrong F length (should be " + expectedLength + " bytes): " + f.length);
}
} else {
} else if (f.length <= l) {
throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
"Key encapsulation only supported for XDH");
"Strange F length: " + f.length + " <= " + l);
}
dh.setF(Arrays.copyOfRange(f, l, f.length));
Digest keyHash = dh.getHash();
Expand All @@ -170,6 +171,7 @@ public boolean next(int cmd, Buffer buffer) throws Exception {
keyHash.update(dh.getK());
k = keyHash.digest();
} catch (IllegalArgumentException ex) {
log.error("Key encapsulation error", ex);
throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
"Key encapsulation error: " + ex.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ public class BaseBuilder<T extends AbstractFactoryManager, S extends BaseBuilder
Arrays.asList(
BuiltinDHFactories.sntrup761x25519,
BuiltinDHFactories.sntrup761x25519_openssh,
BuiltinDHFactories.mlkem768x25519,
BuiltinDHFactories.mlkem1024nistp384,
BuiltinDHFactories.mlkem768nistp256,
BuiltinDHFactories.curve25519,
BuiltinDHFactories.curve25519_libssh,
BuiltinDHFactories.curve448,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,86 @@ public boolean isSupported() {
return MontgomeryCurve.x448.isSupported() && BuiltinDigests.sha512.isSupported();
}
},
/**
* @see <a href= "https://datatracker.ietf.org/doc/html/draft-kampanakis-curdle-ssh-pq-ke-04">PQ/T Hybrid Key
* Exchange in SSH</a>
*/
mlkem768x25519(Constants.MLKEM768_25519_SHA256) {
@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, true) {

@Override
public KeyEncapsulationMethod getKeyEncapsulation() {
return BuiltinKEM.mlkem768;
}

@Override
public Digest getHash() throws Exception {
return BuiltinDigests.sha256.create();
}
};
}

@Override
public boolean isSupported() {
return MontgomeryCurve.x25519.isSupported() && BuiltinDigests.sha256.isSupported()
&& BuiltinKEM.mlkem768.isSupported();
}
},
/**
* @see <a href= "https://datatracker.ietf.org/doc/html/draft-kampanakis-curdle-ssh-pq-ke-04">PQ/T Hybrid Key
* Exchange in SSH</a>
*/
mlkem768nistp256(Constants.MLKEM768_NISTP256_SHA256) {
@Override
public ECDH create(Object... params) throws Exception {
if (!GenericUtils.isEmpty(params)) {
throw new IllegalArgumentException("No accepted parameters for " + getName());
}
return new ECDH(ECCurves.nistp256, true) {

@Override
public KeyEncapsulationMethod getKeyEncapsulation() {
return BuiltinKEM.mlkem768;
}

};
}

@Override
public boolean isSupported() {
return ECCurves.nistp256.isSupported() && BuiltinKEM.mlkem768.isSupported();
}
},
/**
* @see <a href= "https://datatracker.ietf.org/doc/html/draft-kampanakis-curdle-ssh-pq-ke-04">PQ/T Hybrid Key
* Exchange in SSH</a>
*/
mlkem1024nistp384(Constants.MLKEM1024_NISTP384_SHA384) {
@Override
public ECDH create(Object... params) throws Exception {
if (!GenericUtils.isEmpty(params)) {
throw new IllegalArgumentException("No accepted parameters for " + getName());
}
return new ECDH(ECCurves.nistp384, true) {

@Override
public KeyEncapsulationMethod getKeyEncapsulation() {
return BuiltinKEM.mlkem1024;
}

};
}

@Override
public boolean isSupported() {
return ECCurves.nistp384.isSupported() && BuiltinKEM.mlkem1024.isSupported();
}
},
/**
* @see <a href=
* "https://www.ietf.org/archive/id/draft-josefsson-ntruprime-ssh-02.html">draft-josefsson-ntruprime-ssh-02.html</a>
Expand Down Expand Up @@ -524,6 +604,9 @@ public static final class Constants {
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 MLKEM768_25519_SHA256 = "mlkem768x25519-sha256";
public static final String MLKEM768_NISTP256_SHA256 = "mlkem768nistp256-sha256";
public static final String MLKEM1024_NISTP384_SHA384 = "mlkem1024nistp384-sha384";
public static final String SNTRUP761_25519_SHA512 = "sntrup761x25519-sha512";
public static final String SNTRUP761_25519_SHA512_OPENSSH = SNTRUP761_25519_SHA512 + "@openssh.com";

Expand Down
38 changes: 38 additions & 0 deletions sshd-core/src/main/java/org/apache/sshd/common/kex/BuiltinKEM.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,44 @@
*/
public enum BuiltinKEM implements KeyEncapsulationMethod, NamedResource, OptionalFeature {

mlkem768("mlkem768") {

@Override
public Client getClient() {
return MLKEM.getClient(MLKEM.Parameters.mlkem768);
}

@Override
public Server getServer() {
return MLKEM.getServer(MLKEM.Parameters.mlkem768);
}

@Override
public boolean isSupported() {
return MLKEM.Parameters.mlkem768.isSupported();
}

},

mlkem1024("mlkem1024") {

@Override
public Client getClient() {
return MLKEM.getClient(MLKEM.Parameters.mlkem1024);
}

@Override
public Server getServer() {
return MLKEM.getServer(MLKEM.Parameters.mlkem1024);
}

@Override
public boolean isSupported() {
return MLKEM.Parameters.mlkem1024.isSupported();
}

},

sntrup761("sntrup761") {

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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;

/**
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
*/
public interface CurveSizeIndicator {

/**
* Retrieves the length of a point coordinate in bytes.
*
* @return the length
*/
int getByteLength();
}
45 changes: 26 additions & 19 deletions sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,41 @@
public class ECDH extends AbstractDH {
public static final String KEX_TYPE = "ECDH";

private final boolean raw;

private ECCurves curve;
private ECParameterSpec params;
private ECPoint f;

public ECDH() throws Exception {
this((ECParameterSpec) null);
}

public ECDH(String curveName) throws Exception {
this(ValidateUtils.checkNotNull(ECCurves.fromCurveName(curveName), "Unknown curve name: %s", curveName));
this(curveName, false);
}

public ECDH(ECCurves curve) throws Exception {
this(Objects.requireNonNull(curve, "No known curve instance provided").getParameters());
this.curve = curve;
this(curve, false);
}

public ECDH(ECParameterSpec paramSpec) throws Exception {
this(paramSpec, false);
}

public ECDH(String curveName, boolean raw) throws Exception {
this(ValidateUtils.checkNotNull(ECCurves.fromCurveName(curveName), "Unknown curve name: %s", curveName), raw);
}

public ECDH(ECCurves curve, boolean raw) throws Exception {
this(Objects.requireNonNull(curve, "No known curve instance provided").getParameters(), raw);
this.curve = curve;
}

public ECDH(ECParameterSpec paramSpec, boolean raw) throws Exception {
myKeyAgree = SecurityUtils.getKeyAgreement(KEX_TYPE);
params = paramSpec; // do not check for null-ity since in some cases it can be
params = Objects.requireNonNull(paramSpec, "No EC curve parameters provided");
this.raw = raw;
}

@Override
protected byte[] calculateE() throws Exception {
Objects.requireNonNull(params, "No ECParameterSpec(s)");
KeyPairGenerator myKpairGen = SecurityUtils.getKeyPairGenerator(KeyUtils.EC_ALGORITHM);
myKpairGen.initialize(params);

Expand All @@ -81,22 +91,17 @@ protected byte[] calculateE() throws Exception {

@Override
protected byte[] calculateK() throws Exception {
Objects.requireNonNull(params, "No ECParameterSpec(s)");
Objects.requireNonNull(f, "Missing 'f' value");
ECPublicKeySpec keySpec = new ECPublicKeySpec(f, params);
KeyFactory myKeyFac = SecurityUtils.getKeyFactory(KeyUtils.EC_ALGORITHM);
PublicKey yourPubKey = myKeyFac.generatePublic(keySpec);
myKeyAgree.doPhase(yourPubKey, true);
return stripLeadingZeroes(myKeyAgree.generateSecret());
}

public void setCurveParameters(ECParameterSpec paramSpec) {
params = paramSpec;
byte[] secret = myKeyAgree.generateSecret();
return raw ? secret : stripLeadingZeroes(secret);
}

@Override
public void setF(byte[] f) {
Objects.requireNonNull(params, "No ECParameterSpec(s)");
Objects.requireNonNull(f, "No 'f' value specified");
this.f = ECCurves.octetStringToEcPoint(f);
}
Expand All @@ -117,12 +122,14 @@ public void putF(Buffer buffer, byte[] f) {

@Override
public Digest getHash() throws Exception {
return findCurve().getDigestForParams();
}

private ECCurves findCurve() {
if (curve == null) {
Objects.requireNonNull(params, "No ECParameterSpec(s)");
curve = Objects.requireNonNull(ECCurves.fromCurveParameters(params), "Unknown curve parameters");
}

return curve.getDigestForParams();
return curve;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ interface Client {
*/
interface Server {

/**
* Retrieves the required length of the KEM public key, in bytes.
*
* @return the length of the key
*/
int getPublicKeyLength();

/**
* Initializes the KEM with a public key received from a client and prepares an encapsulated secret.
*
Expand Down
Loading

0 comments on commit 38bb2c6

Please sign in to comment.