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 extends HASH> 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 extends KEM> k = Class.forName(session.getConfig(kem_name)).asSubclass(KEM.class);
+ kem = k.getDeclaredConstructor().newInstance();
+ kem.init();
+
+ Class extends XDH> 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