Skip to content

Commit

Permalink
Implement the "sntrup761x25519-sha512@openssh.com" KEX method
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tomaswolf committed May 9, 2024
1 parent de3f3b2 commit 4f2ccf8
Show file tree
Hide file tree
Showing 16 changed files with 466 additions and 42 deletions.
7 changes: 7 additions & 0 deletions CHANGES.md
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion docs/standards.md
Expand Up @@ -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

Expand Down Expand Up @@ -95,8 +96,10 @@ aes128-gcm@openssh.com, aes256-gcm@openssh.com, chacha20-poly1305@openssh.com, 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, 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

Expand Down
51 changes: 47 additions & 4 deletions sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -55,6 +59,8 @@ public class DHGClient extends AbstractDHClientKeyExchange {
protected final DHFactory factory;
protected AbstractDH dh;

private KeyEncapsulationMethod.Client kemClient;

protected DHGClient(DHFactory factory, Session session) {
super(session);

Expand Down Expand Up @@ -95,7 +101,20 @@ public void init(byte[] v_s, byte[] v_c, byte[] i_s, byte[] i_c) throws Exceptio
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()) {
Expand Down Expand Up @@ -129,8 +148,32 @@ public boolean next(int cmd, Buffer buffer) throws Exception {
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();
Expand Down Expand Up @@ -167,7 +210,7 @@ public boolean next(int cmd, Buffer buffer) throws Exception {
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();

Expand Down
Expand Up @@ -202,7 +202,7 @@ public boolean next(int cmd, Buffer buffer) throws Exception {
validateFValue();

dh.setF(f);
k = dh.getK();
k = normalize(dh.getK());

buffer = new ByteArrayBuffer(k_s);
PublicKey serverKey = buffer.getRawPublicKey();
Expand All @@ -226,7 +226,7 @@ public boolean next(int cmd, Buffer buffer) throws Exception {
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();

Expand Down
Expand Up @@ -87,6 +87,7 @@ public class BaseBuilder<T extends AbstractFactoryManager, S extends BaseBuilder
*/
public static final List<BuiltinDHFactories> DEFAULT_KEX_PREFERENCE = Collections.unmodifiableList(
Arrays.asList(
BuiltinDHFactories.sntrup761x25519,
BuiltinDHFactories.curve25519,
BuiltinDHFactories.curve25519_libssh,
BuiltinDHFactories.curve448,
Expand Down
Expand Up @@ -118,6 +118,10 @@ protected void checkKeyAgreementNecessity() {

public abstract Digest getHash() throws Exception;

public KeyEncapsulationMethod getKeyEncapsulation() {
return null;
}

@Override
public String toString() {
return getClass().getSimpleName()
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -252,12 +253,19 @@ public XDH create(Object... params) throws Exception {
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) {
Expand All @@ -266,12 +274,19 @@ public XDH create(Object... params) throws Exception {
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();
}
},
/**
Expand All @@ -283,12 +298,48 @@ public XDH create(Object... params) throws Exception {
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() && 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.x448.isSupported();
return MontgomeryCurve.x25519.isSupported() && BuiltinDigests.sha512.isSupported()
&& BuiltinKEM.sntrup761.isSupported();
}
};

Expand Down Expand Up @@ -468,6 +519,7 @@ 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 SNTRUP761_25519_SHA512 = "sntrup761x25519-sha512@openssh.com";

private Constants() {
throw new UnsupportedOperationException("No instance allowed");
Expand Down
59 changes: 59 additions & 0 deletions 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;
}

}

0 comments on commit 4f2ccf8

Please sign in to comment.