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 3, 2024
1 parent 5fa5b64 commit a71c329
Show file tree
Hide file tree
Showing 15 changed files with 519 additions and 41 deletions.
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 @@ -88,6 +88,10 @@ public class BaseBuilder<T extends AbstractFactoryManager, S extends BaseBuilder
public static final List<BuiltinDHFactories> DEFAULT_KEX_PREFERENCE = Collections.unmodifiableList(
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:dev@mina.apache.org">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 a71c329

Please sign in to comment.