diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 55e530ea..22d61ccf 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -49,7 +49,7 @@ jobs: java-version: ${{ matrix.java }} check-latest: true - name: Test with Maven - run: ./mvnw -B -V -e -P coverage verify -Denforcer.skip=true -Dmaven.resources.skip=true -Dmaven.main.skip=true -Dassembly.skipAssembly=true -Dmaven.javadoc.skip=true -DskipITs=false + run: ./mvnw -B -V -e -P coverage verify -Denforcer.skip=true -Dmaven.resources.skip=true -Dmaven.main.skip=true -Dbnd.skip=true -Dassembly.skipAssembly=true -Dmaven.javadoc.skip=true -Dcyclonedx.skip=true -DskipITs=false - uses: actions/upload-artifact@v4 with: name: java-${{ matrix.java }}-testresults diff --git a/pom.xml b/pom.xml index 5da91821..4df83993 100644 --- a/pom.xml +++ b/pom.xml @@ -47,12 +47,14 @@ +9 + mwiede Matthias Wiedemann mwiede@gmx.de Community https://github.com/mwiede + norrisjeremy Jeremy Norris Community https://github.com/norrisjeremy @@ -254,7 +256,7 @@ - 16 + 17 @@ -403,31 +405,32 @@ - org.apache.felix - maven-bundle-plugin - 5.1.9 + biz.aQute.bnd + bnd-maven-plugin + 7.0.0 - - com.jcraft.jsch;-noimport:=true - - + ]]> - - - bundle-manifest - process-classes - - manifest - - - org.apache.maven.plugins @@ -661,6 +664,9 @@ jdk-non-portable jdk-reflection + + com.jcraft.jsch.annotations.SuppressForbiddenApi + @@ -859,5 +865,28 @@ + + bnd + + [17,) + + + + + biz.aQute.bnd + bnd-maven-plugin + + + bnd-process + process-classes + + bnd-process + + + + + + + diff --git a/src/main/java/com/jcraft/jsch/DH25519SNTRUP761.java b/src/main/java/com/jcraft/jsch/DH25519SNTRUP761.java new file mode 100644 index 00000000..1c6732ce --- /dev/null +++ b/src/main/java/com/jcraft/jsch/DH25519SNTRUP761.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2015-2018 ymnk, JCraft,Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. The names of the authors may not be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JCRAFT, INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jcraft.jsch; + +class DH25519SNTRUP761 extends DHXECKEM { + public DH25519SNTRUP761() { + kem_name = "sntrup761"; + sha_name = "sha-512"; + curve_name = "X25519"; + kem_pubkey_len = 1158; + kem_encap_len = 1039; + xec_key_len = 32; + } +} diff --git a/src/main/java/com/jcraft/jsch/DHECN.java b/src/main/java/com/jcraft/jsch/DHECN.java index f28c9ff8..a20abf06 100644 --- a/src/main/java/com/jcraft/jsch/DHECN.java +++ b/src/main/java/com/jcraft/jsch/DHECN.java @@ -133,8 +133,7 @@ public boolean next(Buffer _buf) throws Exception { return false; } - K = ecdh.getSecret(r_s[0], r_s[1]); - K = normalize(K); + K = encodeAsMPInt(normalize(ecdh.getSecret(r_s[0], r_s[1]))); byte[] sig_of_H = _buf.getString(); @@ -159,11 +158,11 @@ public boolean next(Buffer _buf) throws Exception { buf.putString(K_S); buf.putString(Q_C); buf.putString(Q_S); - buf.putMPInt(K); byte[] foo = new byte[buf.getLength()]; buf.getByte(foo); sha.update(foo, 0, foo.length); + sha.update(K, 0, K.length); H = sha.digest(); i = 0; diff --git a/src/main/java/com/jcraft/jsch/DHGEX.java b/src/main/java/com/jcraft/jsch/DHGEX.java index 84aa51d4..15f9bd0f 100644 --- a/src/main/java/com/jcraft/jsch/DHGEX.java +++ b/src/main/java/com/jcraft/jsch/DHGEX.java @@ -175,7 +175,7 @@ public boolean next(Buffer _buf) throws Exception { dh.checkRange(); - K = normalize(dh.getK()); + K = encodeAsMPInt(normalize(dh.getK())); // The hash H is computed as the HASH hash of the concatenation of the // following: @@ -208,11 +208,11 @@ public boolean next(Buffer _buf) throws Exception { buf.putMPInt(g); buf.putMPInt(e); buf.putMPInt(f); - buf.putMPInt(K); byte[] foo = new byte[buf.getLength()]; buf.getByte(foo); sha.update(foo, 0, foo.length); + sha.update(K, 0, K.length); H = sha.digest(); diff --git a/src/main/java/com/jcraft/jsch/DHGN.java b/src/main/java/com/jcraft/jsch/DHGN.java index 44aaadf8..ddb41dfb 100644 --- a/src/main/java/com/jcraft/jsch/DHGN.java +++ b/src/main/java/com/jcraft/jsch/DHGN.java @@ -134,7 +134,7 @@ public boolean next(Buffer _buf) throws Exception { dh.checkRange(); - K = normalize(dh.getK()); + K = encodeAsMPInt(normalize(dh.getK())); // The hash H is computed as the HASH hash of the concatenation of the // following: @@ -156,10 +156,11 @@ public boolean next(Buffer _buf) throws Exception { buf.putString(K_S); buf.putMPInt(e); buf.putMPInt(f); - buf.putMPInt(K); byte[] foo = new byte[buf.getLength()]; buf.getByte(foo); + sha.update(foo, 0, foo.length); + sha.update(K, 0, K.length); H = sha.digest(); // System.err.print("H -> "); //dump(H, 0, H.length); diff --git a/src/main/java/com/jcraft/jsch/DHXEC.java b/src/main/java/com/jcraft/jsch/DHXEC.java index a3576189..b8cf55cc 100644 --- a/src/main/java/com/jcraft/jsch/DHXEC.java +++ b/src/main/java/com/jcraft/jsch/DHXEC.java @@ -131,8 +131,7 @@ public boolean next(Buffer _buf) throws Exception { return false; } - K = xdh.getSecret(Q_S); - K = normalize(K); + K = encodeAsMPInt(normalize(xdh.getSecret(Q_S))); byte[] sig_of_H = _buf.getString(); @@ -171,11 +170,11 @@ public boolean next(Buffer _buf) throws Exception { buf.putString(K_S); buf.putString(Q_C); buf.putString(Q_S); - buf.putMPInt(K); byte[] foo = new byte[buf.getLength()]; buf.getByte(foo); sha.update(foo, 0, foo.length); + sha.update(K, 0, K.length); H = sha.digest(); i = 0; diff --git a/src/main/java/com/jcraft/jsch/DHXECKEM.java b/src/main/java/com/jcraft/jsch/DHXECKEM.java new file mode 100644 index 00000000..1bee11fb --- /dev/null +++ b/src/main/java/com/jcraft/jsch/DHXECKEM.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2015-2018 ymnk, JCraft,Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. The names of the authors may not be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JCRAFT, INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jcraft.jsch; + +abstract class DHXECKEM extends KeyExchange { + + private static final int SSH_MSG_KEX_ECDH_INIT = 30; + private static final int SSH_MSG_KEX_ECDH_REPLY = 31; + private int state; + + byte[] Q_C; + + byte[] V_S; + byte[] V_C; + byte[] I_S; + byte[] I_C; + + byte[] e; + + private Buffer buf; + private Packet packet; + + private KEM kem; + private XDH xdh; + + protected String kem_name; + protected String sha_name; + protected String curve_name; + protected int kem_pubkey_len; + protected int kem_encap_len; + protected int xec_key_len; + + @Override + public void init(Session session, byte[] V_S, byte[] V_C, byte[] I_S, byte[] I_C) + throws Exception { + this.V_S = V_S; + this.V_C = V_C; + this.I_S = I_S; + this.I_C = I_C; + + try { + Class c = Class.forName(session.getConfig(sha_name)).asSubclass(HASH.class); + sha = c.getDeclaredConstructor().newInstance(); + sha.init(); + } catch (Exception e) { + throw new JSchException(e.toString(), e); + } + + buf = new Buffer(); + packet = new Packet(buf); + + packet.reset(); + // command + string len + Q_C len + buf.checkFreeSize(1 + 4 + kem_pubkey_len + xec_key_len); + buf.putByte((byte) SSH_MSG_KEX_ECDH_INIT); + + try { + Class k = Class.forName(session.getConfig(kem_name)).asSubclass(KEM.class); + kem = k.getDeclaredConstructor().newInstance(); + kem.init(); + + Class c = Class.forName(session.getConfig("xdh")).asSubclass(XDH.class); + xdh = c.getDeclaredConstructor().newInstance(); + xdh.init(curve_name, xec_key_len); + + byte[] kem_public_key_C = kem.getPublicKey(); + byte[] xec_public_key_C = xdh.getQ(); + Q_C = new byte[kem_pubkey_len + xec_key_len]; + System.arraycopy(kem_public_key_C, 0, Q_C, 0, kem_pubkey_len); + System.arraycopy(xec_public_key_C, 0, Q_C, kem_pubkey_len, xec_key_len); + buf.putString(Q_C); + } catch (Exception | NoClassDefFoundError e) { + throw new JSchException(e.toString(), e); + } + + if (V_S == null) { // This is a really ugly hack for Session.checkKexes ;-( + return; + } + + session.write(packet); + + if (session.getLogger().isEnabled(Logger.INFO)) { + session.getLogger().log(Logger.INFO, "SSH_MSG_KEX_ECDH_INIT sent"); + session.getLogger().log(Logger.INFO, "expecting SSH_MSG_KEX_ECDH_REPLY"); + } + + state = SSH_MSG_KEX_ECDH_REPLY; + } + + @Override + public boolean next(Buffer _buf) throws Exception { + int i, j; + switch (state) { + case SSH_MSG_KEX_ECDH_REPLY: + // The server responds with: + // byte SSH_MSG_KEX_ECDH_REPLY + // string K_S, server's public host key + // string Q_S, server's ephemeral public key octet string + // string the signature on the exchange hash + j = _buf.getInt(); + j = _buf.getByte(); + j = _buf.getByte(); + if (j != SSH_MSG_KEX_ECDH_REPLY) { + if (session.getLogger().isEnabled(Logger.ERROR)) { + session.getLogger().log(Logger.ERROR, "type: must be SSH_MSG_KEX_ECDH_REPLY " + j); + } + return false; + } + + K_S = _buf.getString(); + + byte[] Q_S = _buf.getString(); + if (Q_S.length != kem_encap_len + xec_key_len) { + return false; + } + + byte[] encapsulation = new byte[kem_encap_len]; + byte[] xec_public_key_S = new byte[xec_key_len]; + System.arraycopy(Q_S, 0, encapsulation, 0, kem_encap_len); + System.arraycopy(Q_S, kem_encap_len, xec_public_key_S, 0, xec_key_len); + + // RFC 5656, + // 4. ECDH Key Exchange + // All elliptic curve public keys MUST be validated after they are + // received. An example of a validation algorithm can be found in + // Section 3.2.2 of [SEC1]. If a key fails validation, + // the key exchange MUST fail. + if (!xdh.validate(xec_public_key_S)) { + return false; + } + + byte[] tmp = null; + try { + tmp = kem.decapsulate(encapsulation); + sha.update(tmp, 0, tmp.length); + } finally { + Util.bzero(tmp); + } + try { + tmp = normalize(xdh.getSecret(xec_public_key_S)); + sha.update(tmp, 0, tmp.length); + } finally { + Util.bzero(tmp); + } + K = encodeAsString(sha.digest()); + + byte[] sig_of_H = _buf.getString(); + + // The hash H is computed as the HASH hash of the concatenation of the + // following: + // string V_C, client's identification string (CR and LF excluded) + // string V_S, server's identification string (CR and LF excluded) + // string I_C, payload of the client's SSH_MSG_KEXINIT + // string I_S, payload of the server's SSH_MSG_KEXINIT + // string K_S, server's public host key + // string Q_C, client's ephemeral public key octet string + // string Q_S, server's ephemeral public key octet string + // string K, shared secret + + // draft-josefsson-ntruprime-ssh-02, + // 3. Key Exchange Method: sntrup761x25519-sha512 + // ... + // The SSH_MSG_KEX_ECDH_REPLY's signature value is computed as described + // in [RFC5656] with the following changes. Instead of encoding the + // shared secret K as 'mpint', it MUST be encoded as 'string'. The + // shared secret K value MUST be the 64-byte output octet string of the + // SHA-512 hash computed with the input as the 32-byte octet string key + // output from the key encapsulation mechanism of sntrup761 concatenated + // with the 32-byte octet string of X25519(a, X25519(b, 9)) = X25519(b, + // X25519(a, 9)). + buf.reset(); + buf.putString(V_C); + buf.putString(V_S); + buf.putString(I_C); + buf.putString(I_S); + buf.putString(K_S); + buf.putString(Q_C); + buf.putString(Q_S); + byte[] foo = new byte[buf.getLength()]; + buf.getByte(foo); + + sha.update(foo, 0, foo.length); + sha.update(K, 0, K.length); + H = sha.digest(); + + i = 0; + j = 0; + j = ((K_S[i++] << 24) & 0xff000000) | ((K_S[i++] << 16) & 0x00ff0000) + | ((K_S[i++] << 8) & 0x0000ff00) | ((K_S[i++]) & 0x000000ff); + String alg = Util.byte2str(K_S, i, j); + i += j; + + boolean result = verify(alg, K_S, i, sig_of_H); + + state = STATE_END; + return result; + } + return false; + } + + @Override + public int getState() { + return state; + } +} diff --git a/src/main/java/com/jcraft/jsch/JSch.java b/src/main/java/com/jcraft/jsch/JSch.java index 0c86f25c..a089dd66 100644 --- a/src/main/java/com/jcraft/jsch/JSch.java +++ b/src/main/java/com/jcraft/jsch/JSch.java @@ -103,6 +103,9 @@ public class JSch { config.put("curve25519-sha256", "com.jcraft.jsch.DH25519"); config.put("curve25519-sha256@libssh.org", "com.jcraft.jsch.DH25519"); config.put("curve448-sha512", "com.jcraft.jsch.DH448"); + config.put("sntrup761x25519-sha512@openssh.com", "com.jcraft.jsch.DH25519SNTRUP761"); + + config.put("sntrup761", "com.jcraft.jsch.bc.SNTRUP761"); config.put("dh", "com.jcraft.jsch.jce.DH"); config.put("3des-cbc", "com.jcraft.jsch.jce.TripleDESCBC"); @@ -239,7 +242,7 @@ public class JSch { Util.getSystemProperty("jsch.check_ciphers", "chacha20-poly1305@openssh.com")); config.put("CheckMacs", Util.getSystemProperty("jsch.check_macs", "")); config.put("CheckKexes", Util.getSystemProperty("jsch.check_kexes", - "curve25519-sha256,curve25519-sha256@libssh.org,curve448-sha512")); + "sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,curve448-sha512")); config.put("CheckSignatures", Util.getSystemProperty("jsch.check_signatures", "ssh-ed25519,ssh-ed448")); config.put("FingerprintHash", Util.getSystemProperty("jsch.fingerprint_hash", "sha256")); diff --git a/src/main/java/com/jcraft/jsch/KEM.java b/src/main/java/com/jcraft/jsch/KEM.java new file mode 100644 index 00000000..90dcaacf --- /dev/null +++ b/src/main/java/com/jcraft/jsch/KEM.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2015-2018 ymnk, JCraft,Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. The names of the authors may not be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JCRAFT, INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jcraft.jsch; + +public interface KEM { + void init() throws Exception; + + byte[] getPublicKey() throws Exception; + + byte[] decapsulate(byte[] encapsulation) throws Exception; +} diff --git a/src/main/java/com/jcraft/jsch/KeyExchange.java b/src/main/java/com/jcraft/jsch/KeyExchange.java index e686be76..3090391e 100644 --- a/src/main/java/com/jcraft/jsch/KeyExchange.java +++ b/src/main/java/com/jcraft/jsch/KeyExchange.java @@ -215,6 +215,11 @@ byte[] getK() { return K; } + void clearK() { + Util.bzero(K); + K = null; + } + byte[] getH() { return H; } @@ -230,16 +235,59 @@ byte[] getHostKey() { /* * It seems JCE included in Oracle's Java7u6(and later) has suddenly changed its behavior. The * secrete generated by KeyAgreement#generateSecret() may start with 0, even if it is a positive - * value. + * value. See https://bugs.openjdk.org/browse/JDK-7146728. */ protected byte[] normalize(byte[] secret) { - if (secret.length > 1 && secret[0] == 0 && (secret[1] & 0x80) == 0) { - byte[] tmp = new byte[secret.length - 1]; - System.arraycopy(secret, 1, tmp, 0, tmp.length); - return normalize(tmp); - } else { + // This should be a timing safe version of the following: + // if (secret.length > 1 && secret[0] == 0 && (secret[1] & 0x80) == 0) { + // byte[] tmp = new byte[secret.length - 1]; + // System.arraycopy(secret, 1, tmp, 0, tmp.length); + // Util.bzero(secret); + // return normalize(tmp); + // } else { + // return secret; + // } + + int len = secret.length; + if (len < 2) { return secret; } + + // secret[0] == 0 + int a = 0; + int s0 = secret[0] & 0xff; + for (int i = 0; i < 8; i++) { + int j = s0 >>> i; + j &= 0x1; + a |= j; + } + a ^= 0x1; + + // (secret[1..n] & 0x80) == 0 && secret[1..n] != 0 + int offset = 0; + for (int i = 1; i < len; i++) { + int j = secret[i] & 0x80; + j >>>= 7; + j ^= 0x1; + a &= j; + offset += a; + j = secret[i] & 0x7f; + for (int k = 0; k < 7; k++) { + int l = j >>> k; + l &= 0x1; + l ^= 0x1; + a &= l; + } + } + + len -= offset; + // Try to remain timing safe by performing an allocation + copy for leading bytes removed + byte[] foo = new byte[len]; + byte[] bar = new byte[offset]; + System.arraycopy(secret, 0, bar, 0, offset); + System.arraycopy(secret, offset, foo, 0, len); + Util.bzero(secret); + return foo; } protected boolean verify(String alg, byte[] K_S, int index, byte[] sig_of_H) throws Exception { @@ -425,4 +473,30 @@ protected boolean verify(String alg, byte[] K_S, int index, byte[] sig_of_H) thr return result; } + protected byte[] encodeAsMPInt(byte[] raw) { + int i = (raw[0] & 0x80) >>> 7; + int len = raw.length + i; + byte[] foo = new byte[len + 4]; + // Try to remain timing safe by performing an extra allocation when i == 0 + byte[] bar = new byte[i ^ 0x1]; + foo[0] = (byte) (len >>> 24); + foo[1] = (byte) (len >>> 16); + foo[2] = (byte) (len >>> 8); + foo[3] = (byte) (len); + System.arraycopy(raw, 0, foo, 4 + i, len - i); + Util.bzero(raw); + return foo; + } + + protected byte[] encodeAsString(byte[] raw) { + int len = raw.length; + byte[] foo = new byte[len + 4]; + foo[0] = (byte) (len >>> 24); + foo[1] = (byte) (len >>> 16); + foo[2] = (byte) (len >>> 8); + foo[3] = (byte) (len); + System.arraycopy(raw, 0, foo, 4, len); + Util.bzero(raw); + return foo; + } } diff --git a/src/main/java/com/jcraft/jsch/Session.java b/src/main/java/com/jcraft/jsch/Session.java index 57302763..b2464fad 100644 --- a/src/main/java/com/jcraft/jsch/Session.java +++ b/src/main/java/com/jcraft/jsch/Session.java @@ -1461,7 +1461,11 @@ byte[] getSessionId() { } private void receive_newkeys(Buffer buf, KeyExchange kex) throws Exception { - updateKeys(kex); + try { + updateKeys(kex); + } finally { + kex.clearK(); + } in_kex = false; if (doStrictKex) { seqi = 0; @@ -1491,7 +1495,7 @@ private void updateKeys(KeyExchange kex) throws Exception { */ buf.reset(); - buf.putMPInt(K); + buf.putByte(K); buf.putByte(H); buf.putByte((byte) 0x41); buf.putByte(session_id); @@ -1530,7 +1534,7 @@ private void updateKeys(KeyExchange kex) throws Exception { s2ccipher = cc.getDeclaredConstructor().newInstance(); while (s2ccipher.getBlockSize() > Es2c.length) { buf.reset(); - buf.putMPInt(K); + buf.putByte(K); buf.putByte(H); buf.putByte(Es2c); hash.update(buf.buffer, 0, buf.index); @@ -1559,7 +1563,7 @@ private void updateKeys(KeyExchange kex) throws Exception { c2scipher = cc.getDeclaredConstructor().newInstance(); while (c2scipher.getBlockSize() > Ec2s.length) { buf.reset(); - buf.putMPInt(K); + buf.putByte(K); buf.putByte(H); buf.putByte(Ec2s); hash.update(buf.buffer, 0, buf.index); @@ -1608,7 +1612,7 @@ private byte[] expandKey(Buffer buf, byte[] K, byte[] H, byte[] key, HASH hash, int size = hash.getBlockSize(); while (result.length < required_length) { buf.reset(); - buf.putMPInt(K); + buf.putByte(K); buf.putByte(H); buf.putByte(result); hash.update(buf.buffer, 0, buf.index); diff --git a/src/main/java/com/jcraft/jsch/annotations/SuppressForbiddenApi.java b/src/main/java/com/jcraft/jsch/annotations/SuppressForbiddenApi.java new file mode 100644 index 00000000..b3494689 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/annotations/SuppressForbiddenApi.java @@ -0,0 +1,13 @@ +package com.jcraft.jsch.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, + ElementType.PARAMETER, ElementType.TYPE}) +public @interface SuppressForbiddenApi { + String[] value(); +} diff --git a/src/main/java/com/jcraft/jsch/bc/SNTRUP761.java b/src/main/java/com/jcraft/jsch/bc/SNTRUP761.java new file mode 100644 index 00000000..08862a07 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/bc/SNTRUP761.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2015-2018 ymnk, JCraft,Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. The names of the authors may not be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JCRAFT, INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jcraft.jsch.bc; + +import com.jcraft.jsch.KEM; +import com.jcraft.jsch.annotations.SuppressForbiddenApi; +import java.lang.reflect.Constructor; +import java.security.SecureRandom; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.pqc.crypto.ntruprime.*; + +public class SNTRUP761 implements KEM { + SNTRUPrimeKEMExtractor extractor; + SNTRUPrimePublicKeyParameters publicKey; + + @Override + public void init() throws Exception { + SNTRUPrimeKeyPairGenerator kpg = new SNTRUPrimeKeyPairGenerator(); + kpg.init(new SNTRUPrimeKeyGenerationParameters(new SecureRandom(), sntrup761())); + AsymmetricCipherKeyPair kp = kpg.generateKeyPair(); + extractor = new SNTRUPrimeKEMExtractor((SNTRUPrimePrivateKeyParameters) kp.getPrivate()); + publicKey = (SNTRUPrimePublicKeyParameters) kp.getPublic(); + } + + @Override + public byte[] getPublicKey() throws Exception { + return publicKey.getEncoded(); + } + + @Override + public byte[] decapsulate(byte[] encapsulation) throws Exception { + return extractor.extractSecret(encapsulation); + } + + // Bouncy Castle before 1.78 defines sharedKeyBytes differently than OpenSSH (16 instead of 32) + // https://github.com/bcgit/bc-java/issues/1554 + // https://github.com/bcgit/bc-java/commit/db3ae60 + @SuppressForbiddenApi("jdk-reflection") + static SNTRUPrimeParameters sntrup761() throws Exception { + if (SNTRUPrimeParameters.sntrup761.getSessionKeySize() == 32 * 8) { + return SNTRUPrimeParameters.sntrup761; + } + Constructor c = + SNTRUPrimeParameters.class.getDeclaredConstructor(String.class, int.class, int.class, + int.class, int.class, int.class, int.class, int.class, int.class); + c.setAccessible(true); + return c.newInstance("sntrup761", 761, 4591, 286, 1158, 1007, 1158, 1763, 32); + } +} diff --git a/src/main/java/com/jcraft/jsch/jce/DH.java b/src/main/java/com/jcraft/jsch/jce/DH.java index 114102ea..c8a7f888 100644 --- a/src/main/java/com/jcraft/jsch/jce/DH.java +++ b/src/main/java/com/jcraft/jsch/jce/DH.java @@ -39,8 +39,6 @@ public class DH implements com.jcraft.jsch.DH { BigInteger e; // my public key byte[] e_array; BigInteger f; // your public key - BigInteger K; // shared secret key - byte[] K_array; private KeyPairGenerator myKpairGen; private KeyAgreement myKeyAgree; @@ -66,17 +64,11 @@ public byte[] getE() throws Exception { @Override public byte[] getK() throws Exception { - if (K == null) { - KeyFactory myKeyFac = KeyFactory.getInstance("DH"); - DHPublicKeySpec keySpec = new DHPublicKeySpec(f, p, g); - PublicKey yourPubKey = myKeyFac.generatePublic(keySpec); - myKeyAgree.doPhase(yourPubKey, true); - byte[] mySharedSecret = myKeyAgree.generateSecret(); - K = new BigInteger(1, mySharedSecret); - K_array = K.toByteArray(); - K_array = mySharedSecret; - } - return K_array; + KeyFactory myKeyFac = KeyFactory.getInstance("DH"); + DHPublicKeySpec keySpec = new DHPublicKeySpec(f, p, g); + PublicKey yourPubKey = myKeyFac.generatePublic(keySpec); + myKeyAgree.doPhase(yourPubKey, true); + return myKeyAgree.generateSecret(); } @Override diff --git a/src/test/java/com/jcraft/jsch/Algorithms4IT.java b/src/test/java/com/jcraft/jsch/Algorithms4IT.java new file mode 100644 index 00000000..0d5632f9 --- /dev/null +++ b/src/test/java/com/jcraft/jsch/Algorithms4IT.java @@ -0,0 +1,191 @@ +package com.jcraft.jsch; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.github.valfirst.slf4jtest.LoggingEvent; +import com.github.valfirst.slf4jtest.TestLogger; +import com.github.valfirst.slf4jtest.TestLoggerFactory; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Random; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +public class Algorithms4IT { + + private static final int timeout = 2000; + private static final DigestUtils sha256sum = new DigestUtils(DigestUtils.getSha256Digest()); + private static final TestLogger jschLogger = TestLoggerFactory.getTestLogger(JSch.class); + private static final TestLogger sshdLogger = TestLoggerFactory.getTestLogger(Algorithms4IT.class); + + @TempDir + public Path tmpDir; + private Path in; + private Path out; + private String hash; + private Slf4jLogConsumer sshdLogConsumer; + + @Container + public GenericContainer sshd = new GenericContainer<>( + new ImageFromDockerfile().withFileFromClasspath("ssh_host_rsa_key", "docker/ssh_host_rsa_key") + .withFileFromClasspath("ssh_host_rsa_key.pub", "docker/ssh_host_rsa_key.pub") + .withFileFromClasspath("ssh_host_ecdsa256_key", "docker/ssh_host_ecdsa256_key") + .withFileFromClasspath("ssh_host_ecdsa256_key.pub", "docker/ssh_host_ecdsa256_key.pub") + .withFileFromClasspath("ssh_host_ecdsa384_key", "docker/ssh_host_ecdsa384_key") + .withFileFromClasspath("ssh_host_ecdsa384_key.pub", "docker/ssh_host_ecdsa384_key.pub") + .withFileFromClasspath("ssh_host_ecdsa521_key", "docker/ssh_host_ecdsa521_key") + .withFileFromClasspath("ssh_host_ecdsa521_key.pub", "docker/ssh_host_ecdsa521_key.pub") + .withFileFromClasspath("ssh_host_ed25519_key", "docker/ssh_host_ed25519_key") + .withFileFromClasspath("ssh_host_ed25519_key.pub", "docker/ssh_host_ed25519_key.pub") + .withFileFromClasspath("ssh_host_dsa_key", "docker/ssh_host_dsa_key") + .withFileFromClasspath("ssh_host_dsa_key.pub", "docker/ssh_host_dsa_key.pub") + .withFileFromClasspath("sshd_config", "docker/sshd_config.openssh96") + .withFileFromClasspath("authorized_keys", "docker/authorized_keys") + .withFileFromClasspath("Dockerfile", "docker/Dockerfile.openssh96")) + .withExposedPorts(22); + + @BeforeAll + public static void beforeAll() { + JSch.setLogger(new Slf4jLogger()); + } + + @BeforeEach + public void beforeEach() throws IOException { + if (sshdLogConsumer == null) { + sshdLogConsumer = new Slf4jLogConsumer(sshdLogger); + sshd.followOutput(sshdLogConsumer); + } + + in = tmpDir.resolve("in"); + out = tmpDir.resolve("out"); + Files.createFile(in); + try (OutputStream os = Files.newOutputStream(in)) { + byte[] data = new byte[1024]; + for (int i = 0; i < 1024 * 100; i += 1024) { + new Random().nextBytes(data); + os.write(data); + } + } + hash = sha256sum.digestAsHex(in); + + jschLogger.clearAll(); + sshdLogger.clearAll(); + } + + @AfterAll + public static void afterAll() { + JSch.setLogger(null); + jschLogger.clearAll(); + sshdLogger.clearAll(); + } + + @ParameterizedTest + @ValueSource(strings = {"sntrup761x25519-sha512@openssh.com"}) + public void testBCKEXs(String kex) throws Exception { + JSch ssh = createRSAIdentity(); + Session session = createSession(ssh); + session.setConfig("kex", kex); + doSftp(session, true); + + String expected = String.format(Locale.ROOT, "kex: algorithm: %s.*", kex); + checkLogs(expected); + } + + private JSch createRSAIdentity() throws Exception { + HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub")); + JSch ssh = new JSch(); + ssh.addIdentity(getResourceFile("docker/id_rsa"), getResourceFile("docker/id_rsa.pub"), null); + ssh.getHostKeyRepository().add(hostKey, null); + return ssh; + } + + private HostKey readHostKey(String fileName) throws Exception { + List lines = Files.readAllLines(Paths.get(fileName), UTF_8); + String[] split = lines.get(0).split("\\s+"); + String hostname = + String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort()); + return new HostKey(hostname, Base64.getDecoder().decode(split[1])); + } + + private Session createSession(JSch ssh) throws Exception { + Session session = ssh.getSession("root", sshd.getHost(), sshd.getFirstMappedPort()); + session.setConfig("StrictHostKeyChecking", "yes"); + session.setConfig("PreferredAuthentications", "publickey"); + return session; + } + + private void doSftp(Session session, boolean debugException) throws Exception { + try { + session.setTimeout(timeout); + session.connect(); + ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp"); + sftp.connect(timeout); + sftp.put(in.toString(), "/root/test"); + sftp.get("/root/test", out.toString()); + sftp.disconnect(); + session.disconnect(); + } catch (Exception e) { + if (debugException) { + printInfo(); + } + throw e; + } + + assertEquals(1024L * 100L, Files.size(out)); + assertEquals(hash, sha256sum.digestAsHex(out)); + } + + private void printInfo() { + jschLogger.getAllLoggingEvents().stream().map(LoggingEvent::getFormattedMessage) + .forEach(System.out::println); + sshdLogger.getAllLoggingEvents().stream().map(LoggingEvent::getFormattedMessage) + .forEach(System.out::println); + System.out.println(""); + System.out.println(""); + System.out.println(""); + } + + private void checkLogs(String expected) { + Optional actualJsch = jschLogger.getAllLoggingEvents().stream() + .map(LoggingEvent::getFormattedMessage).filter(msg -> msg.matches(expected)).findFirst(); + // Skip OpenSSH log checks, as log output from Docker falls behind and these assertions + // frequently run before they are output + // Optional actualSshd = + // sshdLogger.getAllLoggingEvents().stream() + // .map(LoggingEvent::getFormattedMessage) + // .filter(msg -> msg.matches("STDERR: debug1: " + expected)) + // .findFirst(); + try { + assertTrue(actualJsch.isPresent(), () -> "JSch: " + expected); + // assertTrue(actualSshd.isPresent(), () -> "sshd: " + expected); + } catch (AssertionError e) { + printInfo(); + throw e; + } + } + + private String getResourceFile(String fileName) { + return ResourceUtil.getResourceFile(getClass(), fileName); + } +} diff --git a/src/test/java/com/jcraft/jsch/KeyExchangeTest.java b/src/test/java/com/jcraft/jsch/KeyExchangeTest.java new file mode 100644 index 00000000..ef5aae3c --- /dev/null +++ b/src/test/java/com/jcraft/jsch/KeyExchangeTest.java @@ -0,0 +1,249 @@ +package com.jcraft.jsch; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.Arrays; +import java.util.Random; +import org.junit.jupiter.api.Test; + +public class KeyExchangeTest { + + private final Random random = new Random(); + private final KeyExchange kex = new TestKex(); + + @Test + public void testNormalize0() { + byte[] secret = new byte[0]; + doNormalize(secret); + } + + @Test + public void testNormalize1() { + KeyExchange kex = new TestKex(); + byte[] secret = new byte[1]; + for (int i = 0; i <= 0xff; i++) { + secret[0] = (byte) i; + doNormalize(secret); + } + } + + @Test + public void testNormalize2() { + KeyExchange kex = new TestKex(); + byte[] secret = new byte[2]; + for (int i = 0; i <= 0xff; i++) { + secret[0] = (byte) i; + for (int j = 0; j <= 0xff; j++) { + secret[1] = (byte) j; + doNormalize(secret); + } + } + } + + @Test + public void testNormalize3() { + KeyExchange kex = new TestKex(); + byte[] secret = new byte[3]; + for (int i = 0; i <= 0xff; i++) { + secret[0] = (byte) i; + for (int j = 0; j <= 0xff; j++) { + secret[1] = (byte) j; + for (int k = 0; k <= 0xff; k++) { + secret[2] = (byte) k; + doNormalize(secret); + } + } + } + } + + @Test + public void testNormalizeRandom() { + KeyExchange kex = new TestKex(); + for (int i = 0; i < 1000000; i++) { + byte[] secret = new byte[64]; + random.nextBytes(secret); + doNormalize(secret); + } + } + + @Test + public void testEncodeAsMPInt1() { + KeyExchange kex = new TestKex(); + byte[] secret = new byte[1]; + for (int i = 0; i <= 0xff; i++) { + secret[0] = (byte) i; + doEncodeAsMPInt(secret); + } + } + + @Test + public void testEncodeAsMPInt2() { + KeyExchange kex = new TestKex(); + byte[] secret = new byte[2]; + for (int i = 0; i <= 0xff; i++) { + secret[0] = (byte) i; + for (int j = 0; j <= 0xff; j++) { + secret[1] = (byte) j; + doEncodeAsMPInt(secret); + } + } + } + + @Test + public void testEncodeAsMPInt3() { + KeyExchange kex = new TestKex(); + byte[] secret = new byte[3]; + for (int i = 0; i <= 0xff; i++) { + secret[0] = (byte) i; + for (int j = 0; j <= 0xff; j++) { + secret[1] = (byte) j; + for (int k = 0; k <= 0xff; k++) { + secret[2] = (byte) k; + doEncodeAsMPInt(secret); + } + } + } + } + + @Test + public void testEncodeAsMPIntRandom() { + KeyExchange kex = new TestKex(); + for (int i = 0; i < 1000000; i++) { + byte[] secret = new byte[64]; + random.nextBytes(secret); + doEncodeAsMPInt(secret); + } + } + + @Test + public void testEncodeAsString0() { + KeyExchange kex = new TestKex(); + byte[] secret = new byte[0]; + doEncodeAsString(secret); + } + + @Test + public void testEncodeAsString1() { + KeyExchange kex = new TestKex(); + byte[] secret = new byte[1]; + for (int i = 0; i <= 0xff; i++) { + secret[0] = (byte) i; + doEncodeAsString(secret); + } + } + + @Test + public void testEncodeAsString2() { + KeyExchange kex = new TestKex(); + byte[] secret = new byte[2]; + for (int i = 0; i <= 0xff; i++) { + secret[0] = (byte) i; + for (int j = 0; j <= 0xff; j++) { + secret[1] = (byte) j; + doEncodeAsString(secret); + } + } + } + + @Test + public void testEncodeAsString3() { + KeyExchange kex = new TestKex(); + byte[] secret = new byte[3]; + for (int i = 0; i <= 0xff; i++) { + secret[0] = (byte) i; + for (int j = 0; j <= 0xff; j++) { + secret[1] = (byte) j; + for (int k = 0; k <= 0xff; k++) { + secret[2] = (byte) k; + doEncodeAsString(secret); + } + } + } + } + + @Test + public void testEncodeAsStringRandom() { + KeyExchange kex = new TestKex(); + for (int i = 0; i < 1000000; i++) { + byte[] secret = new byte[64]; + random.nextBytes(secret); + doEncodeAsString(secret); + } + } + + private void doNormalize(byte[] secret) { + byte[] expected = normalize(Arrays.copyOf(secret, secret.length)); + byte[] actual = kex.normalize(Arrays.copyOf(secret, secret.length)); + try { + assertArrayEquals(expected, actual); + } catch (Exception e) { + System.out.println(" secret = " + Arrays.toString(secret)); + System.out.println("expected = " + Arrays.toString(expected)); + System.out.println(" actual = " + Arrays.toString(actual)); + throw e; + } + } + + // Copy of old implementation + private static byte[] normalize(byte[] secret) { + if (secret.length > 1 && secret[0] == 0 && (secret[1] & 0x80) == 0) { + byte[] tmp = new byte[secret.length - 1]; + System.arraycopy(secret, 1, tmp, 0, tmp.length); + Util.bzero(secret); + return normalize(tmp); + } else { + return secret; + } + } + + private void doEncodeAsMPInt(byte[] secret) { + Buffer b = new Buffer(); + b.putMPInt(secret); + byte[] expected = new byte[b.getLength()]; + b.getByte(expected); + byte[] actual = kex.encodeAsMPInt(Arrays.copyOf(secret, secret.length)); + try { + assertArrayEquals(expected, actual); + } catch (Throwable t) { + System.out.println(" secret = " + Arrays.toString(secret)); + System.out.println("expected = " + Arrays.toString(expected)); + System.out.println(" actual = " + Arrays.toString(actual)); + throw t; + } + } + + private void doEncodeAsString(byte[] secret) { + Buffer b = new Buffer(); + b.putString(secret); + byte[] expected = new byte[b.getLength()]; + b.getByte(expected); + byte[] actual = kex.encodeAsString(Arrays.copyOf(secret, secret.length)); + try { + assertArrayEquals(expected, actual); + } catch (Throwable t) { + System.out.println(" secret = " + Arrays.toString(secret)); + System.out.println("expected = " + Arrays.toString(expected)); + System.out.println(" actual = " + Arrays.toString(actual)); + throw t; + } + } + + static class TestKex extends KeyExchange { + + @Override + public void init(Session session, byte[] V_S, byte[] V_C, byte[] I_S, byte[] I_C) + throws Exception { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public boolean next(Buffer buf) throws Exception { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public int getState() { + throw new UnsupportedOperationException("Not supported"); + } + } +} diff --git a/src/test/resources/docker/sshd_config.ExtInfoInAuthIT b/src/test/resources/docker/sshd_config.ExtInfoInAuthIT index d63c7f83..73dd194a 100644 --- a/src/test/resources/docker/sshd_config.ExtInfoInAuthIT +++ b/src/test/resources/docker/sshd_config.ExtInfoInAuthIT @@ -14,7 +14,7 @@ HostKey /etc/ssh/ssh_host_ecdsa521_key HostKey /etc/ssh/ssh_host_ed25519_key HostKey /etc/ssh/ssh_host_rsa_key HostKey /etc/ssh/ssh_host_dsa_key -KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group18-sha512,diffie-hellman-group16-sha512,diffie-hellman-group14-sha256,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1 +KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group18-sha512,diffie-hellman-group16-sha512,diffie-hellman-group14-sha256,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1 HostKeyAlgorithms ecdsa-sha2-nistp521,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa,ssh-dss Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-sha1,hmac-sha1-96-etm@openssh.com,hmac-sha1-96,hmac-md5-etm@openssh.com,hmac-md5,hmac-md5-96-etm@openssh.com,hmac-md5-96 diff --git a/src/test/resources/docker/sshd_config.openssh96 b/src/test/resources/docker/sshd_config.openssh96 index 8d7bcba1..12c4064f 100644 --- a/src/test/resources/docker/sshd_config.openssh96 +++ b/src/test/resources/docker/sshd_config.openssh96 @@ -14,7 +14,7 @@ HostKey /etc/ssh/ssh_host_ecdsa521_key HostKey /etc/ssh/ssh_host_ed25519_key HostKey /etc/ssh/ssh_host_rsa_key HostKey /etc/ssh/ssh_host_dsa_key -KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group18-sha512,diffie-hellman-group16-sha512,diffie-hellman-group14-sha256,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1 +KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group18-sha512,diffie-hellman-group16-sha512,diffie-hellman-group14-sha256,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1 HostKeyAlgorithms ecdsa-sha2-nistp521,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa,ssh-dss Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-sha1,hmac-sha1-96-etm@openssh.com,hmac-sha1-96,hmac-md5-etm@openssh.com,hmac-md5,hmac-md5-96-etm@openssh.com,hmac-md5-96