diff --git a/CHANGES.md b/CHANGES.md index 05724a426..7da6113e1 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 c2d8f28f8..9998f66c1 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 @@ 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 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 294f3c733..9235ffbf6 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 @@ public class DHGClient extends AbstractDHClientKeyExchange { protected final DHFactory factory; protected AbstractDH dh; + private KeyEncapsulationMethod.Client kemClient; + protected DHGClient(DHFactory factory, Session session) { super(session); @@ -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()) { @@ -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(); @@ -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(); 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 3452bbc0d..ac9eecf78 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 @@ 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(); @@ -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(); 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 3386e8085..0c2ecc950 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 class BaseBuilder 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 5480b4430..b9de78f52 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 @@ protected void checkKeyAgreementNecessity() { 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 3d7e85420..c0c3c5a21 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 @@ 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) { @@ -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(); } }, /** @@ -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 draft-josefsson-ntruprime-ssh-02.html + */ + 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(); } }; @@ -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"); 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 000000000..33997f52f --- /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 000000000..a83612738 --- /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 1b7684ed5..3f6904f47 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 @@ public enum MontgomeryCurve implements KeySizeIndicator, OptionalFeature { /** * 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 @@ public KeyAgreement createKeyAgreement() throws GeneralSecurityException { 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 000000000..81df2340c --- /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 5d3fcab04..f321251c0 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 RFC 8731 */ -public class XDH extends AbstractDH { +public abstract class XDH extends AbstractDH { protected MontgomeryCurve curve; protected byte[] f; @@ -40,6 +39,10 @@ public XDH(MontgomeryCurve curve) throws Exception { myKeyAgree = curve.createKeyAgreement(); } + public int getKeySize() { + return curve.getKeySize(); + } + @Override protected byte[] calculateE() throws Exception { KeyPair keyPair = curve.generateKeyPair(); @@ -76,9 +79,4 @@ protected byte[] calculateK() throws Exception { 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 746484b67..2f5e01936 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 @@ protected void validateFValue(BigInteger pValue) throws SshException { 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 b05a3ab92..921e28733 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 @@ protected void prepareNewKeys() throws Exception { } 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 6ba71ca91..7b6c6e2bd 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 @@ public boolean next(int cmd, Buffer buffer) throws Exception { 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 @@ public boolean next(int cmd, Buffer buffer) throws Exception { 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 3d1e02be9..2d90fcccf 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 @@ public boolean next(int cmd, Buffer buffer) throws Exception { } 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 @@ public boolean next(int cmd, Buffer buffer) throws Exception { 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();