diff --git a/pom.xml b/pom.xml
index 1f815b11..3dc16283 100644
--- a/pom.xml
+++ b/pom.xml
@@ -650,6 +650,19 @@
+
+ de.thetaphi
+ forbiddenapis
+ 3.6
+
+
+ jdk-unsafe
+ jdk-deprecated
+ jdk-non-portable
+ jdk-reflection
+
+
+
@@ -805,5 +818,46 @@
+
+ forbiddenapis
+
+ [16,)
+
+
+
+
+ de.thetaphi
+ forbiddenapis
+
+ 16
+
+
+
+ check
+
+ check
+
+
+
+ jdk-system-out
+
+
+
+
+ testCheck
+
+ testCheck
+
+
+
+ commons-io-unsafe-2.14.0
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jcraft/jsch/HostKey.java b/src/main/java/com/jcraft/jsch/HostKey.java
index 51b6b8f1..0f9922b6 100644
--- a/src/main/java/com/jcraft/jsch/HostKey.java
+++ b/src/main/java/com/jcraft/jsch/HostKey.java
@@ -26,6 +26,8 @@
package com.jcraft.jsch;
+import java.util.Locale;
+
public class HostKey {
private static final byte[][] names =
@@ -118,7 +120,7 @@ public String getKey() {
public String getFingerPrint(JSch jsch) {
HASH hash = null;
try {
- String _c = JSch.getConfig("FingerprintHash").toLowerCase();
+ String _c = JSch.getConfig("FingerprintHash").toLowerCase(Locale.ROOT);
Class extends HASH> c = Class.forName(JSch.getConfig(_c)).asSubclass(HASH.class);
hash = c.getDeclaredConstructor().newInstance();
} catch (Exception e) {
diff --git a/src/main/java/com/jcraft/jsch/JSch.java b/src/main/java/com/jcraft/jsch/JSch.java
index c1335964..0c86f25c 100644
--- a/src/main/java/com/jcraft/jsch/JSch.java
+++ b/src/main/java/com/jcraft/jsch/JSch.java
@@ -45,8 +45,12 @@ public class JSch {
"ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256"));
config.put("prefer_known_host_key_types",
Util.getSystemProperty("jsch.prefer_known_host_key_types", "yes"));
+ config.put("enable_strict_kex", Util.getSystemProperty("jsch.enable_strict_kex", "yes"));
+ config.put("require_strict_kex", Util.getSystemProperty("jsch.require_strict_kex", "no"));
config.put("enable_server_sig_algs",
Util.getSystemProperty("jsch.enable_server_sig_algs", "yes"));
+ config.put("enable_ext_info_in_auth",
+ Util.getSystemProperty("jsch.enable_ext_info_in_auth", "yes"));
config.put("cipher.s2c", Util.getSystemProperty("jsch.cipher",
"aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com"));
config.put("cipher.c2s", Util.getSystemProperty("jsch.cipher",
diff --git a/src/main/java/com/jcraft/jsch/JSchAlgoNegoFailException.java b/src/main/java/com/jcraft/jsch/JSchAlgoNegoFailException.java
index fbdbf446..6e668250 100644
--- a/src/main/java/com/jcraft/jsch/JSchAlgoNegoFailException.java
+++ b/src/main/java/com/jcraft/jsch/JSchAlgoNegoFailException.java
@@ -1,5 +1,7 @@
package com.jcraft.jsch;
+import java.util.Locale;
+
/**
* Extension of {@link JSchException} to indicate when a connection fails during algorithm
* negotiation.
@@ -35,7 +37,7 @@ public String getServerProposal() {
}
private static String failString(int algorithmIndex, String jschProposal, String serverProposal) {
- return String.format(
+ return String.format(Locale.ROOT,
"Algorithm negotiation fail: algorithmName=\"%s\" jschProposal=\"%s\" serverProposal=\"%s\"",
algorithmNameFromIndex(algorithmIndex), jschProposal, serverProposal);
}
diff --git a/src/main/java/com/jcraft/jsch/JSchStrictKexException.java b/src/main/java/com/jcraft/jsch/JSchStrictKexException.java
new file mode 100644
index 00000000..3454c1d2
--- /dev/null
+++ b/src/main/java/com/jcraft/jsch/JSchStrictKexException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2002-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 class JSchStrictKexException extends JSchException {
+ private static final long serialVersionUID = -1L;
+
+ JSchStrictKexException() {
+ super();
+ }
+
+ JSchStrictKexException(String s) {
+ super(s);
+ }
+}
diff --git a/src/main/java/com/jcraft/jsch/KeyExchange.java b/src/main/java/com/jcraft/jsch/KeyExchange.java
index 7a3d508e..e686be76 100644
--- a/src/main/java/com/jcraft/jsch/KeyExchange.java
+++ b/src/main/java/com/jcraft/jsch/KeyExchange.java
@@ -26,6 +26,8 @@
package com.jcraft.jsch;
+import java.util.Locale;
+
public abstract class KeyExchange {
static final int PROPOSAL_KEX_ALGS = 0;
@@ -198,7 +200,7 @@ protected static String[] guess(Session session, byte[] I_S, byte[] I_C) throws
public String getFingerPrint() {
HASH hash = null;
try {
- String _c = session.getConfig("FingerprintHash").toLowerCase();
+ String _c = session.getConfig("FingerprintHash").toLowerCase(Locale.ROOT);
Class extends HASH> c = Class.forName(session.getConfig(_c)).asSubclass(HASH.class);
hash = c.getDeclaredConstructor().newInstance();
} catch (Exception e) {
diff --git a/src/main/java/com/jcraft/jsch/OpenSSHConfig.java b/src/main/java/com/jcraft/jsch/OpenSSHConfig.java
index 4dd6ff05..c56f63a8 100644
--- a/src/main/java/com/jcraft/jsch/OpenSSHConfig.java
+++ b/src/main/java/com/jcraft/jsch/OpenSSHConfig.java
@@ -36,6 +36,7 @@
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
+import java.util.Locale;
import java.util.Set;
import java.util.Vector;
import java.util.stream.Collectors;
@@ -75,9 +76,10 @@
*/
public class OpenSSHConfig implements ConfigRepository {
- private static final Set keysWithListAdoption =
- Stream.of("KexAlgorithms", "Ciphers", "HostKeyAlgorithms", "MACs", "PubkeyAcceptedAlgorithms",
- "PubkeyAcceptedKeyTypes").map(String::toUpperCase).collect(Collectors.toSet());
+ private static final Set keysWithListAdoption = Stream
+ .of("KexAlgorithms", "Ciphers", "HostKeyAlgorithms", "MACs", "PubkeyAcceptedAlgorithms",
+ "PubkeyAcceptedKeyTypes")
+ .map(string -> string.toUpperCase(Locale.ROOT)).collect(Collectors.toSet());
/**
* Parses the given string, and returns an instance of ConfigRepository.
@@ -209,13 +211,13 @@ private String find(String key) {
if (keymap.get(key) != null) {
key = keymap.get(key);
}
- key = key.toUpperCase();
+ key = key.toUpperCase(Locale.ROOT);
String value = null;
for (int i = 0; i < _configs.size(); i++) {
Vector v = _configs.elementAt(i);
for (int j = 0; j < v.size(); j++) {
String[] kv = v.elementAt(j);
- if (kv[0].toUpperCase().equals(key)) {
+ if (kv[0].toUpperCase(Locale.ROOT).equals(key)) {
value = kv[1];
break;
}
@@ -255,13 +257,13 @@ private String find(String key) {
}
private String[] multiFind(String key) {
- key = key.toUpperCase();
+ key = key.toUpperCase(Locale.ROOT);
Vector value = new Vector<>();
for (int i = 0; i < _configs.size(); i++) {
Vector v = _configs.elementAt(i);
for (int j = 0; j < v.size(); j++) {
String[] kv = v.elementAt(j);
- if (kv[0].toUpperCase().equals(key)) {
+ if (kv[0].toUpperCase(Locale.ROOT).equals(key)) {
String foo = kv[1];
if (foo != null) {
value.remove(foo);
diff --git a/src/main/java/com/jcraft/jsch/PageantConnector.java b/src/main/java/com/jcraft/jsch/PageantConnector.java
index 30511c40..898a8aab 100644
--- a/src/main/java/com/jcraft/jsch/PageantConnector.java
+++ b/src/main/java/com/jcraft/jsch/PageantConnector.java
@@ -40,6 +40,7 @@
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.platform.win32.WinUser;
import com.sun.jna.platform.win32.WinUser.COPYDATASTRUCT;
+import java.util.Locale;
public class PageantConnector implements AgentConnector {
@@ -84,7 +85,8 @@ public void query(Buffer buffer) throws AgentProxyException {
throw new AgentProxyException("Pageant is not runnning.");
}
- String mapname = String.format("PageantRequest%08x", kernel32.GetCurrentThreadId());
+ String mapname =
+ String.format(Locale.ROOT, "PageantRequest%08x", kernel32.GetCurrentThreadId());
HANDLE sharedFile = null;
Pointer sharedMemory = null;
diff --git a/src/main/java/com/jcraft/jsch/Session.java b/src/main/java/com/jcraft/jsch/Session.java
index 47cf3f19..57302763 100644
--- a/src/main/java/com/jcraft/jsch/Session.java
+++ b/src/main/java/com/jcraft/jsch/Session.java
@@ -37,6 +37,7 @@
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
import java.util.Properties;
import java.util.Vector;
@@ -116,6 +117,15 @@ public class Session {
private volatile boolean isConnected = false;
+ private volatile boolean doExtInfo = false;
+ private boolean enable_server_sig_algs = true;
+ private boolean enable_ext_info_in_auth = true;
+
+ private volatile boolean initialKex = true;
+ private volatile boolean doStrictKex = false;
+ private boolean enable_strict_kex = true;
+ private boolean require_strict_kex = false;
+
private volatile boolean isAuthed = false;
private Thread connectThread = null;
@@ -193,6 +203,7 @@ public void connect(int connectTimeout) throws JSchException {
if (isConnected) {
throw new JSchException("session is already connected");
}
+ initialKex = true;
io = new IO();
if (random == null) {
@@ -307,6 +318,10 @@ public void connect(int connectTimeout) throws JSchException {
getLogger().log(Logger.INFO, "Local version string: " + Util.byte2str(V_C));
}
+ enable_server_sig_algs = getConfig("enable_server_sig_algs").equals("yes");
+ enable_ext_info_in_auth = getConfig("enable_ext_info_in_auth").equals("yes");
+ enable_strict_kex = getConfig("enable_strict_kex").equals("yes");
+ require_strict_kex = getConfig("require_strict_kex").equals("yes");
send_kexinit();
buf = read(buf);
@@ -364,9 +379,14 @@ public void connect(int connectTimeout) throws JSchException {
}
receive_newkeys(buf, kex);
+ initialKex = false;
} else {
in_kex = false;
- throw new JSchException("invalid protocol(newkyes): " + buf.getCommand());
+ throw new JSchException("invalid protocol(newkeys): " + buf.getCommand());
+ }
+
+ if (enable_server_sig_algs && enable_ext_info_in_auth && doExtInfo) {
+ send_extinfo();
}
try {
@@ -400,7 +420,7 @@ public void connect(int connectTimeout) throws JSchException {
if (!auth) {
smethods = uan.getMethods();
if (smethods != null) {
- smethods = smethods.toLowerCase();
+ smethods = smethods.toLowerCase(Locale.ROOT);
} else {
// methods: publickey,password,keyboard-interactive
// smethods = "publickey,password,keyboard-interactive";
@@ -564,6 +584,30 @@ private KeyExchange receive_kexinit(Buffer buf) throws Exception {
}
System.arraycopy(buf.buffer, buf.s, I_S, 0, I_S.length);
+ if (initialKex) {
+ if (enable_strict_kex || require_strict_kex) {
+ doStrictKex = checkServerStrictKex();
+ if (doStrictKex) {
+ if (getLogger().isEnabled(Logger.INFO)) {
+ getLogger().log(Logger.INFO, "Doing strict KEX");
+ }
+
+ if (seqi != 1) {
+ throw new JSchStrictKexException("KEXINIT not first packet from server");
+ }
+ } else if (require_strict_kex) {
+ throw new JSchStrictKexException("Strict KEX not supported by server");
+ }
+ }
+
+ if (enable_server_sig_algs) {
+ doExtInfo = checkServerExtInfo();
+ if (getLogger().isEnabled(Logger.INFO)) {
+ getLogger().log(Logger.INFO, "ext-info messaging supported by server");
+ }
+ }
+ }
+
if (!in_kex) { // We are in rekeying activated by the remote!
send_kexinit();
}
@@ -571,7 +615,9 @@ private KeyExchange receive_kexinit(Buffer buf) throws Exception {
guess = KeyExchange.guess(this, I_S, I_C);
if (guess[KeyExchange.PROPOSAL_KEX_ALGS].equals("ext-info-c")
- || guess[KeyExchange.PROPOSAL_KEX_ALGS].equals("ext-info-s")) {
+ || guess[KeyExchange.PROPOSAL_KEX_ALGS].equals("ext-info-s")
+ || guess[KeyExchange.PROPOSAL_KEX_ALGS].equals("kex-strict-c-v00@openssh.com")
+ || guess[KeyExchange.PROPOSAL_KEX_ALGS].equals("kex-strict-s-v00@openssh.com")) {
throw new JSchException("Invalid Kex negotiated: " + guess[KeyExchange.PROPOSAL_KEX_ALGS]);
}
@@ -594,6 +640,50 @@ private KeyExchange receive_kexinit(Buffer buf) throws Exception {
return kex;
}
+ private boolean checkServerStrictKex() {
+ Buffer sb = new Buffer(I_S);
+ sb.setOffSet(17);
+ byte[] sp = sb.getString(); // server proposal
+
+ int l = 0;
+ int m = 0;
+ while (l < sp.length) {
+ while (l < sp.length && sp[l] != ',')
+ l++;
+ if (m == l)
+ continue;
+ if ("kex-strict-s-v00@openssh.com".equals(Util.byte2str(sp, m, l - m))) {
+ return true;
+ }
+ l++;
+ m = l;
+ }
+
+ return false;
+ }
+
+ private boolean checkServerExtInfo() {
+ Buffer sb = new Buffer(I_S);
+ sb.setOffSet(17);
+ byte[] sp = sb.getString(); // server proposal
+
+ int l = 0;
+ int m = 0;
+ while (l < sp.length) {
+ while (l < sp.length && sp[l] != ',')
+ l++;
+ if (m == l)
+ continue;
+ if ("ext-info-s".equals(Util.byte2str(sp, m, l - m))) {
+ return true;
+ }
+ l++;
+ m = l;
+ }
+
+ return false;
+ }
+
private volatile boolean in_kex = false;
private volatile boolean in_prompt = false;
private volatile String[] not_available_shks = null;
@@ -677,11 +767,14 @@ private void send_kexinit() throws Exception {
}
}
- String enable_server_sig_algs = getConfig("enable_server_sig_algs");
- if (enable_server_sig_algs.equals("yes") && !isAuthed) {
+ if (enable_server_sig_algs && !isAuthed) {
kex += ",ext-info-c";
}
+ if ((enable_strict_kex || require_strict_kex) && initialKex) {
+ kex += ",kex-strict-c-v00@openssh.com";
+ }
+
String server_host_key = getConfig("server_host_key");
String[] not_available_shks = checkSignatures(getConfig("CheckSignatures"));
// Cache for UserAuthPublicKey
@@ -809,6 +902,20 @@ private void send_newkeys() throws Exception {
}
}
+ private void send_extinfo() throws Exception {
+ // send SSH_MSG_EXT_INFO(7)
+ packet.reset();
+ buf.putByte((byte) SSH_MSG_EXT_INFO);
+ buf.putInt(1);
+ buf.putString(Util.str2byte("ext-info-in-auth@openssh.com"));
+ buf.putString(Util.str2byte("0"));
+ write(packet);
+
+ if (getLogger().isEnabled(Logger.INFO)) {
+ getLogger().log(Logger.INFO, "SSH_MSG_EXT_INFO sent");
+ }
+ }
+
private void checkHost(String chost, int port, KeyExchange kex) throws JSchException {
String shkc = getConfig("StrictHostKeyChecking");
@@ -1176,7 +1283,9 @@ Buffer read(Buffer buf) throws Exception {
}
}
- seqi++;
+ if (++seqi == 0 && (enable_strict_kex || require_strict_kex) && initialKex) {
+ throw new JSchStrictKexException("incoming sequence number wrapped during initial KEX");
+ }
if (inflater != null) {
// inflater.uncompress(buf);
@@ -1209,6 +1318,8 @@ Buffer read(Buffer buf) throws Exception {
"SSH_MSG_DISCONNECT: " + reason_code + " " + description + " " + language_tag,
reason_code, description, language_tag);
// break;
+ } else if (initialKex && doStrictKex) {
+ break;
} else if (type == SSH_MSG_IGNORE) {
} else if (type == SSH_MSG_UNIMPLEMENTED) {
buf.rewind();
@@ -1241,8 +1352,7 @@ Buffer read(Buffer buf) throws Exception {
buf.getInt();
buf.getShort();
boolean ignore = false;
- String enable_server_sig_algs = getConfig("enable_server_sig_algs");
- if (!enable_server_sig_algs.equals("yes")) {
+ if (!enable_server_sig_algs) {
ignore = true;
if (getLogger().isEnabled(Logger.INFO)) {
getLogger().log(Logger.INFO,
@@ -1353,6 +1463,13 @@ byte[] getSessionId() {
private void receive_newkeys(Buffer buf, KeyExchange kex) throws Exception {
updateKeys(kex);
in_kex = false;
+ if (doStrictKex) {
+ seqi = 0;
+ if (getLogger().isEnabled(Logger.INFO)) {
+ getLogger().log(Logger.INFO,
+ "Reset incoming sequence number after receiving SSH_MSG_NEWKEYS for strict KEX");
+ }
+ }
}
private void updateKeys(KeyExchange kex) throws Exception {
@@ -1620,13 +1737,29 @@ void write(Packet packet) throws Exception {
}
private void _write(Packet packet) throws Exception {
+ boolean initialKex = this.initialKex;
+ boolean doStrictKex = this.doStrictKex;
+ boolean enable_strict_kex = this.enable_strict_kex;
+ boolean require_strict_kex = this.require_strict_kex;
+ boolean resetSeqo = packet.buffer.getCommand() == SSH_MSG_NEWKEYS && doStrictKex;
+
synchronized (lock) {
encode(packet);
if (io != null) {
io.put(packet);
- seqo++;
+ if (++seqo == 0 && (enable_strict_kex || require_strict_kex) && initialKex) {
+ throw new JSchStrictKexException("outgoing sequence number wrapped during initial KEX");
+ }
+ if (resetSeqo) {
+ seqo = 0;
+ }
}
}
+
+ if (resetSeqo && io != null && getLogger().isEnabled(Logger.INFO)) {
+ getLogger().log(Logger.INFO,
+ "Reset outgoing sequence number after sending SSH_MSG_NEWKEYS for strict KEX");
+ }
}
Runnable thread;
@@ -2009,6 +2142,10 @@ public void disconnect() {
// for the first packet during (re)connect.
seqi = 0;
seqo = 0;
+ initialKex = true;
+ doStrictKex = false;
+ doExtInfo = false;
+ serverSigAlgs = null;
// synchronized(jsch.pool){
// jsch.pool.removeElement(this);
@@ -2631,9 +2768,6 @@ public void setConfig(Hashtable newconf) {
String key =
(newkey.equals("PubkeyAcceptedKeyTypes") ? "PubkeyAcceptedAlgorithms" : newkey);
String value = newconf.get(newkey);
- if (key.equals("enable_server_sig_algs") && !value.equals("yes")) {
- serverSigAlgs = null;
- }
config.put(key, value);
}
}
@@ -2647,9 +2781,6 @@ public void setConfig(String key, String value) {
if (key.equals("PubkeyAcceptedKeyTypes")) {
config.put("PubkeyAcceptedAlgorithms", value);
} else {
- if (key.equals("enable_server_sig_algs") && !value.equals("yes")) {
- serverSigAlgs = null;
- }
config.put(key, value);
}
}
@@ -3066,6 +3197,10 @@ private void applyConfig() throws JSchException {
checkConfig(config, "kex");
checkConfig(config, "server_host_key");
checkConfig(config, "prefer_known_host_key_types");
+ checkConfig(config, "enable_server_sig_algs");
+ checkConfig(config, "enable_ext_info_in_auth");
+ checkConfig(config, "enable_strict_kex");
+ checkConfig(config, "require_strict_kex");
checkConfig(config, "enable_pubkey_auth_query");
checkConfig(config, "try_additional_pubkey_algorithms");
checkConfig(config, "enable_auth_none");
diff --git a/src/main/java/com/jcraft/jsch/UserAuthKeyboardInteractive.java b/src/main/java/com/jcraft/jsch/UserAuthKeyboardInteractive.java
index cd5d5eda..2fbabdbe 100644
--- a/src/main/java/com/jcraft/jsch/UserAuthKeyboardInteractive.java
+++ b/src/main/java/com/jcraft/jsch/UserAuthKeyboardInteractive.java
@@ -26,6 +26,8 @@
package com.jcraft.jsch;
+import java.util.Locale;
+
class UserAuthKeyboardInteractive extends UserAuth {
@Override
public boolean start(Session session) throws Exception {
@@ -129,7 +131,7 @@ public boolean start(Session session) throws Exception {
byte[][] response = null;
if (password != null && prompt.length == 1 && !echo[0]
- && prompt[0].toLowerCase().indexOf("password:") >= 0) {
+ && prompt[0].toLowerCase(Locale.ROOT).indexOf("password:") >= 0) {
response = new byte[1][];
response[0] = password;
password = null;
diff --git a/src/main/java/com/jcraft/jsch/jzlib/InflaterInputStream.java b/src/main/java/com/jcraft/jsch/jzlib/InflaterInputStream.java
index 12287c8d..6d5a90bb 100644
--- a/src/main/java/com/jcraft/jsch/jzlib/InflaterInputStream.java
+++ b/src/main/java/com/jcraft/jsch/jzlib/InflaterInputStream.java
@@ -27,6 +27,7 @@
package com.jcraft.jsch.jzlib;
import java.io.*;
+import java.nio.charset.StandardCharsets;
final class InflaterInputStream extends FilterInputStream {
protected final Inflater inflater;
@@ -223,7 +224,7 @@ byte[] getAvailIn() {
void readHeader() throws IOException {
- byte[] empty = "".getBytes();
+ byte[] empty = "".getBytes(StandardCharsets.UTF_8);
inflater.setInput(empty, 0, 0, false);
inflater.setOutput(empty, 0, 0);
diff --git a/src/test/java/com/jcraft/jsch/AbstractBufferMargin.java b/src/test/java/com/jcraft/jsch/AbstractBufferMargin.java
index 508de07c..8b152b88 100644
--- a/src/test/java/com/jcraft/jsch/AbstractBufferMargin.java
+++ b/src/test/java/com/jcraft/jsch/AbstractBufferMargin.java
@@ -14,6 +14,7 @@
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;
+import java.util.Locale;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.input.BoundedInputStream;
@@ -140,7 +141,8 @@ private JSch createRSAIdentity() throws Exception {
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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
+ String hostname =
+ String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
diff --git a/src/test/java/com/jcraft/jsch/Algorithms2IT.java b/src/test/java/com/jcraft/jsch/Algorithms2IT.java
index a070f5d4..6c9e37d0 100644
--- a/src/test/java/com/jcraft/jsch/Algorithms2IT.java
+++ b/src/test/java/com/jcraft/jsch/Algorithms2IT.java
@@ -16,6 +16,7 @@
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;
@@ -146,7 +147,7 @@ public void testKEXs(String kex) throws Exception {
session.setConfig("kex", kex);
doSftp(session, true);
- String expected = String.format("kex: algorithm: %s.*", kex);
+ String expected = String.format(Locale.ROOT, "kex: algorithm: %s.*", kex);
checkLogs(expected);
}
@@ -177,9 +178,9 @@ public void testDHGEXSizes(String kex, String size) throws Exception {
session.setConfig("dhgex_preferred", size);
doSftp(session, true);
- String expectedKex = String.format("kex: algorithm: %s.*", kex);
- String expectedSizes =
- String.format("SSH_MSG_KEX_DH_GEX_REQUEST\\(%s<%s<%s\\) sent", size, size, size);
+ String expectedKex = String.format(Locale.ROOT, "kex: algorithm: %s.*", kex);
+ String expectedSizes = String.format(Locale.ROOT,
+ "SSH_MSG_KEX_DH_GEX_REQUEST\\(%s<%s<%s\\) sent", size, size, size);
checkLogs(expectedKex);
checkLogs(expectedSizes);
}
@@ -235,7 +236,7 @@ public void testRSA(String keyType) throws Exception {
session.setConfig("server_host_key", keyType);
doSftp(session, true);
- String expected = String.format("kex: host key algorithm: %s.*", keyType);
+ String expected = String.format(Locale.ROOT, "kex: host key algorithm: %s.*", keyType);
checkLogs(expected);
}
@@ -250,8 +251,8 @@ public void testCiphers(String cipher, String compression) throws Exception {
session.setConfig("compression.c2s", compression);
doSftp(session, true);
- String expectedS2C = String.format("kex: server->client cipher: %s.*", cipher);
- String expectedC2S = String.format("kex: client->server cipher: %s.*", cipher);
+ String expectedS2C = String.format(Locale.ROOT, "kex: server->client cipher: %s.*", cipher);
+ String expectedC2S = String.format(Locale.ROOT, "kex: client->server cipher: %s.*", cipher);
checkLogs(expectedS2C);
checkLogs(expectedC2S);
}
@@ -274,8 +275,8 @@ public void testMACs(String mac, String compression) throws Exception {
session.setConfig("cipher.c2s", "aes128-ctr");
doSftp(session, true);
- String expectedS2C = String.format("kex: server->client .* MAC: %s.*", mac);
- String expectedC2S = String.format("kex: client->server .* MAC: %s.*", mac);
+ String expectedS2C = String.format(Locale.ROOT, "kex: server->client .* MAC: %s.*", mac);
+ String expectedC2S = String.format(Locale.ROOT, "kex: client->server .* MAC: %s.*", mac);
checkLogs(expectedS2C);
checkLogs(expectedC2S);
}
@@ -304,7 +305,7 @@ public void testCompressionImpls(String impl) throws Exception {
session.setConfig("zlib", impl);
doSftp(session, true);
- String expectedImpl = String.format("zlib using %s", impl);
+ String expectedImpl = String.format(Locale.ROOT, "zlib using %s", impl);
String expectedS2C = "kex: server->client .* compression: zlib.*";
String expectedC2S = "kex: client->server .* compression: zlib.*";
checkLogs(expectedImpl);
@@ -332,7 +333,8 @@ private JSch createEd448Identity() throws Exception {
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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
+ String hostname =
+ String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
diff --git a/src/test/java/com/jcraft/jsch/Algorithms3IT.java b/src/test/java/com/jcraft/jsch/Algorithms3IT.java
index 08705968..3da1f74b 100644
--- a/src/test/java/com/jcraft/jsch/Algorithms3IT.java
+++ b/src/test/java/com/jcraft/jsch/Algorithms3IT.java
@@ -14,6 +14,7 @@
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;
@@ -96,8 +97,8 @@ public void testCiphers(String cipher, String compression) throws Exception {
session.setConfig("compression.c2s", compression);
doSftp(session, true);
- String expectedS2C = String.format("kex: server->client cipher: %s.*", cipher);
- String expectedC2S = String.format("kex: client->server cipher: %s.*", cipher);
+ String expectedS2C = String.format(Locale.ROOT, "kex: server->client cipher: %s.*", cipher);
+ String expectedC2S = String.format(Locale.ROOT, "kex: client->server cipher: %s.*", cipher);
checkLogs(expectedS2C);
checkLogs(expectedC2S);
}
@@ -113,7 +114,8 @@ private JSch createRSAIdentity() throws Exception {
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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
+ String hostname =
+ String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
diff --git a/src/test/java/com/jcraft/jsch/AlgorithmsIT.java b/src/test/java/com/jcraft/jsch/AlgorithmsIT.java
index bc83053f..676970e2 100644
--- a/src/test/java/com/jcraft/jsch/AlgorithmsIT.java
+++ b/src/test/java/com/jcraft/jsch/AlgorithmsIT.java
@@ -18,6 +18,7 @@
import java.util.Arrays;
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;
@@ -115,7 +116,7 @@ public void testJava11KEXs(String kex) throws Exception {
session.setConfig("kex", kex);
doSftp(session, true);
- String expected = String.format("kex: algorithm: %s.*", kex);
+ String expected = String.format(Locale.ROOT, "kex: algorithm: %s.*", kex);
checkLogs(expected);
}
@@ -128,7 +129,7 @@ public void testBCKEXs(String kex) throws Exception {
session.setConfig("kex", kex);
doSftp(session, true);
- String expected = String.format("kex: algorithm: %s.*", kex);
+ String expected = String.format(Locale.ROOT, "kex: algorithm: %s.*", kex);
checkLogs(expected);
}
@@ -144,7 +145,7 @@ public void testKEXs(String kex) throws Exception {
session.setConfig("kex", kex);
doSftp(session, true);
- String expected = String.format("kex: algorithm: %s.*", kex);
+ String expected = String.format(Locale.ROOT, "kex: algorithm: %s.*", kex);
checkLogs(expected);
}
@@ -164,9 +165,9 @@ public void testDHGEXSizes(String kex, String size) throws Exception {
session.setConfig("dhgex_preferred", size);
doSftp(session, true);
- String expectedKex = String.format("kex: algorithm: %s.*", kex);
- String expectedSizes =
- String.format("SSH_MSG_KEX_DH_GEX_REQUEST\\(%s<%s<%s\\) sent", size, size, size);
+ String expectedKex = String.format(Locale.ROOT, "kex: algorithm: %s.*", kex);
+ String expectedSizes = String.format(Locale.ROOT,
+ "SSH_MSG_KEX_DH_GEX_REQUEST\\(%s<%s<%s\\) sent", size, size, size);
checkLogs(expectedKex);
checkLogs(expectedSizes);
}
@@ -257,7 +258,7 @@ public void testRSA(String keyType) throws Exception {
session.setConfig("server_host_key", keyType);
doSftp(session, true);
- String expected = String.format("kex: host key algorithm: %s.*", keyType);
+ String expected = String.format(Locale.ROOT, "kex: host key algorithm: %s.*", keyType);
checkLogs(expected);
}
@@ -296,8 +297,8 @@ public void testCiphers(String cipher, String compression) throws Exception {
session.setConfig("compression.c2s", compression);
doSftp(session, true);
- String expectedS2C = String.format("kex: server->client cipher: %s.*", cipher);
- String expectedC2S = String.format("kex: client->server cipher: %s.*", cipher);
+ String expectedS2C = String.format(Locale.ROOT, "kex: server->client cipher: %s.*", cipher);
+ String expectedC2S = String.format(Locale.ROOT, "kex: client->server cipher: %s.*", cipher);
checkLogs(expectedS2C);
checkLogs(expectedC2S);
}
@@ -329,8 +330,8 @@ public void testMACs(String mac, String compression) throws Exception {
session.setConfig("cipher.c2s", "aes128-ctr");
doSftp(session, true);
- String expectedS2C = String.format("kex: server->client .* MAC: %s.*", mac);
- String expectedC2S = String.format("kex: client->server .* MAC: %s.*", mac);
+ String expectedS2C = String.format(Locale.ROOT, "kex: server->client .* MAC: %s.*", mac);
+ String expectedC2S = String.format(Locale.ROOT, "kex: client->server .* MAC: %s.*", mac);
checkLogs(expectedS2C);
checkLogs(expectedC2S);
}
@@ -344,8 +345,10 @@ public void testCompressions(String compression) throws Exception {
session.setConfig("compression.c2s", compression);
doSftp(session, true);
- String expectedS2C = String.format("kex: server->client .* compression: %s.*", compression);
- String expectedC2S = String.format("kex: client->server .* compression: %s.*", compression);
+ String expectedS2C =
+ String.format(Locale.ROOT, "kex: server->client .* compression: %s.*", compression);
+ String expectedC2S =
+ String.format(Locale.ROOT, "kex: client->server .* compression: %s.*", compression);
checkLogs(expectedS2C);
checkLogs(expectedC2S);
}
@@ -360,7 +363,7 @@ public void testCompressionImpls(String impl) throws Exception {
session.setConfig("zlib@openssh.com", impl);
doSftp(session, true);
- String expectedImpl = String.format("zlib using %s", impl);
+ String expectedImpl = String.format(Locale.ROOT, "zlib using %s", impl);
String expectedS2C = "kex: server->client .* compression: zlib@openssh\\.com.*";
String expectedC2S = "kex: client->server .* compression: zlib@openssh\\.com.*";
checkLogs(expectedImpl);
@@ -392,7 +395,7 @@ public void testFingerprintHashes(String fingerprint) throws Exception {
} catch (JSchException expected) {
}
- String expected = String.format("RSA key fingerprint is %s.", fingerprint);
+ String expected = String.format(Locale.ROOT, "RSA key fingerprint is %s.", fingerprint);
List msgs = userInfo.getMessages().stream().map(msg -> msg.split("\n"))
.flatMap(Arrays::stream).collect(toList());
Optional actual = msgs.stream().filter(msg -> msg.equals(expected)).findFirst();
@@ -460,7 +463,8 @@ private JSch createEd25519Identity() throws Exception {
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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
+ String hostname =
+ String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
diff --git a/src/test/java/com/jcraft/jsch/ExtInfoInAuthIT.java b/src/test/java/com/jcraft/jsch/ExtInfoInAuthIT.java
new file mode 100644
index 00000000..fb58e2c0
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/ExtInfoInAuthIT.java
@@ -0,0 +1,234 @@
+package com.jcraft.jsch;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+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.Test;
+import org.junit.jupiter.api.io.TempDir;
+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 ExtInfoInAuthIT {
+
+ 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(ServerSigAlgsIT.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.ExtInfoInAuthIT")
+ .withFileFromClasspath("authorized_keys", "docker/authorized_keys")
+ .withFileFromClasspath("Dockerfile", "docker/Dockerfile.ExtInfoInAuthIT"))
+ .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();
+ }
+
+ @Test
+ public void testExtInfoInAuthYes() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh, "rsa");
+ session.setConfig("enable_ext_info_in_auth", "yes");
+ session.setConfig("PubkeyAcceptedAlgorithms", "ssh-rsa");
+ doSftp(session, "rsa", true);
+
+ String expectedServerKex = "server proposal: KEX algorithms: .*,ext-info-s,.*";
+ String expectedClientKex = "client proposal: KEX algorithms: .*,ext-info-c,.*";
+ String expected1 = "ext-info messaging supported by server";
+ String expected2 = "SSH_MSG_EXT_INFO sent";
+ String expectedServerSigAlgs1 = "server-sig-algs=";
+ String expectedServerSigAlgs2 = "server-sig-algs=<.*ssh-rsa.*>";
+ String expectedServerSigAlgs3 = "server-sig-algs=<.*ecdsa.*>";
+ checkLogs(expectedServerKex);
+ checkLogs(expectedClientKex);
+ checkLogs(expected1);
+ checkLogs(expected2);
+ checkLogs(expectedServerSigAlgs1);
+ checkLogs(expectedServerSigAlgs2);
+ checkNoLogs(expectedServerSigAlgs3);
+ }
+
+ @Test
+ public void testExtInfoInAuthNo() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh, "ecdsa");
+ session.setConfig("enable_ext_info_in_auth", "no");
+ session.setConfig("PubkeyAcceptedAlgorithms", "ssh-rsa");
+ session.setTimeout(timeout);
+
+ assertThrows(JSchException.class, session::connect, "Auth fail for methods 'publickey'");
+
+ String expectedServerKex = "server proposal: KEX algorithms: .*,ext-info-s,.*";
+ String expectedClientKex = "client proposal: KEX algorithms: .*,ext-info-c,.*";
+ String expected1 = "ext-info messaging supported by server";
+ String expected2 = "SSH_MSG_EXT_INFO sent";
+ String expectedServerSigAlgs1 = "server-sig-algs=";
+ String expectedServerSigAlgs2 = "server-sig-algs=<.*ssh-rsa.*>";
+ String expectedServerSigAlgs3 = "server-sig-algs=<.*ecdsa.*>";
+ checkLogs(expectedServerKex);
+ checkLogs(expectedClientKex);
+ checkLogs(expected1);
+ checkNoLogs(expected2);
+ checkLogs(expectedServerSigAlgs1);
+ checkNoLogs(expectedServerSigAlgs2);
+ checkNoLogs(expectedServerSigAlgs3);
+ }
+
+ 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, String username) throws Exception {
+ Session session = ssh.getSession(username, sshd.getHost(), sshd.getFirstMappedPort());
+ session.setConfig("StrictHostKeyChecking", "yes");
+ session.setConfig("PreferredAuthentications", "publickey");
+ return session;
+ }
+
+ private void doSftp(Session session, String username, boolean debugException) throws Exception {
+ String testFile = String.format(Locale.ROOT, "/%s/test", username);
+ try {
+ session.setTimeout(timeout);
+ session.connect();
+ ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
+ sftp.connect(timeout);
+ sftp.put(in.toString(), testFile);
+ sftp.get(testFile, 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();
+ try {
+ assertTrue(actualJsch.isPresent(), () -> "JSch: " + expected);
+ } catch (AssertionError e) {
+ printInfo();
+ throw e;
+ }
+ }
+
+ private void checkNoLogs(String expected) {
+ Optional actualJsch = jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage).filter(msg -> msg.matches(expected)).findFirst();
+ try {
+ assertFalse(actualJsch.isPresent(), () -> "JSch: " + 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/JSchAlgoNegoFailExceptionIT.java b/src/test/java/com/jcraft/jsch/JSchAlgoNegoFailExceptionIT.java
index 4352136c..5bb4ec66 100644
--- a/src/test/java/com/jcraft/jsch/JSchAlgoNegoFailExceptionIT.java
+++ b/src/test/java/com/jcraft/jsch/JSchAlgoNegoFailExceptionIT.java
@@ -8,6 +8,7 @@
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;
+import java.util.Locale;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.testcontainers.containers.GenericContainer;
@@ -60,9 +61,9 @@ public void testJSchAlgoNegoFailException(String algorithmName, String serverPro
JSchAlgoNegoFailException e = assertThrows(JSchAlgoNegoFailException.class, session::connect);
if (algorithmName.equals("kex")) {
- jschProposal += ",ext-info-c";
+ jschProposal += ",ext-info-c,kex-strict-c-v00@openssh.com";
}
- String message = String.format(
+ String message = String.format(Locale.ROOT,
"Algorithm negotiation fail: algorithmName=\"%s\" jschProposal=\"%s\" serverProposal=\"%s\"",
algorithmName, jschProposal, serverProposal);
@@ -83,7 +84,8 @@ private JSch createRSAIdentity() throws Exception {
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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
+ String hostname =
+ String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
diff --git a/src/test/java/com/jcraft/jsch/JSchStrictKexExceptionIT.java b/src/test/java/com/jcraft/jsch/JSchStrictKexExceptionIT.java
new file mode 100644
index 00000000..e8a7fef6
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/JSchStrictKexExceptionIT.java
@@ -0,0 +1,91 @@
+package com.jcraft.jsch;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Base64;
+import java.util.List;
+import java.util.Locale;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.images.builder.ImageFromDockerfile;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@Testcontainers
+public class JSchStrictKexExceptionIT {
+
+ private static final int timeout = 2000;
+
+ @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")
+ .withFileFromClasspath("authorized_keys", "docker/authorized_keys")
+ .withFileFromClasspath("Dockerfile", "docker/Dockerfile"))
+ .withExposedPorts(22);
+
+ @Test
+ public void testEnableStrictKexRequireStrictKex() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("enable_strict_kex", "yes");
+ session.setConfig("require_strict_kex", "yes");
+ session.setTimeout(timeout);
+
+ assertThrows(JSchStrictKexException.class, session::connect,
+ "Strict KEX not supported by server");
+ }
+
+ @Test
+ public void testNoEnableStrictKexRequireStrictKex() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("enable_strict_kex", "no");
+ session.setConfig("require_strict_kex", "yes");
+ session.setTimeout(timeout);
+
+ assertThrows(JSchStrictKexException.class, session::connect,
+ "Strict KEX not supported by server");
+ }
+
+ 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 String getResourceFile(String fileName) {
+ return ResourceUtil.getResourceFile(getClass(), fileName);
+ }
+}
diff --git a/src/test/java/com/jcraft/jsch/OpenSSH74ServerSigAlgsIT.java b/src/test/java/com/jcraft/jsch/OpenSSH74ServerSigAlgsIT.java
index 104504a1..5b485ab4 100644
--- a/src/test/java/com/jcraft/jsch/OpenSSH74ServerSigAlgsIT.java
+++ b/src/test/java/com/jcraft/jsch/OpenSSH74ServerSigAlgsIT.java
@@ -14,6 +14,7 @@
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;
@@ -143,7 +144,8 @@ private JSch createRSAIdentity() throws Exception {
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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
+ String hostname =
+ String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
diff --git a/src/test/java/com/jcraft/jsch/OpenSSHConfigTest.java b/src/test/java/com/jcraft/jsch/OpenSSHConfigTest.java
index 67bc0b60..bb1bb9be 100644
--- a/src/test/java/com/jcraft/jsch/OpenSSHConfigTest.java
+++ b/src/test/java/com/jcraft/jsch/OpenSSHConfigTest.java
@@ -7,6 +7,7 @@
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Paths;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -15,8 +16,8 @@
class OpenSSHConfigTest {
- Map keyMap = OpenSSHConfig.getKeymap().entrySet().stream().collect(
- Collectors.toMap(entry -> entry.getValue().toUpperCase(), Map.Entry::getKey, (s, s2) -> s2));
+ Map keyMap = OpenSSHConfig.getKeymap().entrySet().stream().collect(Collectors
+ .toMap(entry -> entry.getValue().toUpperCase(Locale.ROOT), Map.Entry::getKey, (s, s2) -> s2));
@Test
void parseFile() throws IOException, URISyntaxException {
@@ -52,7 +53,7 @@ void appendKexAlgorithms() throws IOException {
void appendAlgorithms(String key) throws IOException {
OpenSSHConfig parse = OpenSSHConfig.parse(key + " +someValue,someValue1");
ConfigRepository.Config config = parse.getConfig("");
- String mappedKey = Optional.ofNullable(keyMap.get(key.toUpperCase())).orElse(key);
+ String mappedKey = Optional.ofNullable(keyMap.get(key.toUpperCase(Locale.ROOT))).orElse(key);
assertEquals(JSch.getConfig(mappedKey) + "," + "someValue,someValue1",
config.getValue(mappedKey));
}
@@ -63,7 +64,7 @@ void appendAlgorithms(String key) throws IOException {
void prependAlgorithms(String key) throws IOException {
OpenSSHConfig parse = OpenSSHConfig.parse(key + " ^someValue,someValue1");
ConfigRepository.Config config = parse.getConfig("");
- String mappedKey = Optional.ofNullable(keyMap.get(key.toUpperCase())).orElse(key);
+ String mappedKey = Optional.ofNullable(keyMap.get(key.toUpperCase(Locale.ROOT))).orElse(key);
assertEquals("someValue,someValue1," + JSch.getConfig(mappedKey), config.getValue(mappedKey));
}
diff --git a/src/test/java/com/jcraft/jsch/SSHAgentIT.java b/src/test/java/com/jcraft/jsch/SSHAgentIT.java
index 0ab7c170..57afd010 100644
--- a/src/test/java/com/jcraft/jsch/SSHAgentIT.java
+++ b/src/test/java/com/jcraft/jsch/SSHAgentIT.java
@@ -18,6 +18,7 @@
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;
+import java.util.Locale;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.jupiter.api.AfterAll;
@@ -353,7 +354,8 @@ private JSch createEd25519Identity(USocketFactory factory) throws Exception {
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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
+ String hostname =
+ String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
diff --git a/src/test/java/com/jcraft/jsch/ServerSigAlgsIT.java b/src/test/java/com/jcraft/jsch/ServerSigAlgsIT.java
index 17478043..bc1aa0f5 100644
--- a/src/test/java/com/jcraft/jsch/ServerSigAlgsIT.java
+++ b/src/test/java/com/jcraft/jsch/ServerSigAlgsIT.java
@@ -14,6 +14,7 @@
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;
@@ -139,8 +140,8 @@ public void testNoServerSigAlgs() throws Exception {
doSftp(session, true);
String expectedKex = "kex: host key algorithm: rsa-sha2-512";
- String expectedPubkeysNoServerSigs =
- String.format("No server-sig-algs found, using PubkeyAcceptedAlgorithms = %s", algos);
+ String expectedPubkeysNoServerSigs = String.format(Locale.ROOT,
+ "No server-sig-algs found, using PubkeyAcceptedAlgorithms = %s", algos);
String expectedPreauthFail1 = "ssh-rsa-sha512@ssh.com preauth failure";
String expectedPreauthFail2 = "ssh-rsa-sha384@ssh.com preauth failure";
String expectedPreauthFail3 = "ssh-rsa-sha256@ssh.com preauth failure";
@@ -167,7 +168,8 @@ private JSch createRSAIdentity() throws Exception {
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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
+ String hostname =
+ String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
diff --git a/src/test/java/com/jcraft/jsch/SessionReconnectIT.java b/src/test/java/com/jcraft/jsch/SessionReconnectIT.java
index f2b603ef..80be660a 100644
--- a/src/test/java/com/jcraft/jsch/SessionReconnectIT.java
+++ b/src/test/java/com/jcraft/jsch/SessionReconnectIT.java
@@ -14,6 +14,7 @@
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;
+import java.util.Locale;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.jupiter.api.AfterAll;
@@ -112,7 +113,8 @@ private JSch createRSAIdentity() throws Exception {
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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
+ String hostname =
+ String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
diff --git a/src/test/java/com/jcraft/jsch/StrictKexIT.java b/src/test/java/com/jcraft/jsch/StrictKexIT.java
new file mode 100644
index 00000000..a16f4974
--- /dev/null
+++ b/src/test/java/com/jcraft/jsch/StrictKexIT.java
@@ -0,0 +1,270 @@
+package com.jcraft.jsch;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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.Test;
+import org.junit.jupiter.api.io.TempDir;
+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 StrictKexIT {
+
+ 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(ServerSigAlgsIT.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();
+ }
+
+ @Test
+ public void testEnableStrictKexNoRequireStrictKex() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("enable_strict_kex", "yes");
+ session.setConfig("require_strict_kex", "no");
+ doSftp(session, true);
+
+ String expectedServerKex = "server proposal: KEX algorithms: .*,kex-strict-s-v00@openssh.com";
+ String expectedClientKex = "client proposal: KEX algorithms: .*,kex-strict-c-v00@openssh.com";
+ String expected1 = "Doing strict KEX";
+ String expected2 =
+ "Reset outgoing sequence number after sending SSH_MSG_NEWKEYS for strict KEX";
+ String expected3 =
+ "Reset incoming sequence number after receiving SSH_MSG_NEWKEYS for strict KEX";
+ checkLogs(expectedServerKex);
+ checkLogs(expectedClientKex);
+ checkLogs(expected1);
+ checkLogs(expected2);
+ checkLogs(expected3);
+ }
+
+ @Test
+ public void testEnableStrictKexRequireStrictKex() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("enable_strict_kex", "yes");
+ session.setConfig("require_strict_kex", "yes");
+ doSftp(session, true);
+
+ String expectedServerKex = "server proposal: KEX algorithms: .*,kex-strict-s-v00@openssh.com";
+ String expectedClientKex = "client proposal: KEX algorithms: .*,kex-strict-c-v00@openssh.com";
+ String expected1 = "Doing strict KEX";
+ String expected2 =
+ "Reset outgoing sequence number after sending SSH_MSG_NEWKEYS for strict KEX";
+ String expected3 =
+ "Reset incoming sequence number after receiving SSH_MSG_NEWKEYS for strict KEX";
+ checkLogs(expectedServerKex);
+ checkLogs(expectedClientKex);
+ checkLogs(expected1);
+ checkLogs(expected2);
+ checkLogs(expected3);
+ }
+
+ @Test
+ public void testNoEnableStrictKexRequireStrictKex() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("enable_strict_kex", "no");
+ session.setConfig("require_strict_kex", "yes");
+ doSftp(session, true);
+
+ String expectedServerKex = "server proposal: KEX algorithms: .*,kex-strict-s-v00@openssh.com";
+ String expectedClientKex = "client proposal: KEX algorithms: .*,kex-strict-c-v00@openssh.com";
+ String expected1 = "Doing strict KEX";
+ String expected2 =
+ "Reset outgoing sequence number after sending SSH_MSG_NEWKEYS for strict KEX";
+ String expected3 =
+ "Reset incoming sequence number after receiving SSH_MSG_NEWKEYS for strict KEX";
+ checkLogs(expectedServerKex);
+ checkLogs(expectedClientKex);
+ checkLogs(expected1);
+ checkLogs(expected2);
+ checkLogs(expected3);
+ }
+
+ @Test
+ public void testNoEnableStrictKexNoRequireStrictKex() throws Exception {
+ JSch ssh = createRSAIdentity();
+ Session session = createSession(ssh);
+ session.setConfig("enable_strict_kex", "no");
+ session.setConfig("require_strict_kex", "no");
+ doSftp(session, true);
+
+ String expectedServerKex = "server proposal: KEX algorithms: .*,kex-strict-s-v00@openssh.com";
+ String expectedClientKex = "client proposal: KEX algorithms: .*,kex-strict-c-v00@openssh.com";
+ String expected1 = "Doing strict KEX";
+ String expected2 =
+ "Reset outgoing sequence number after sending SSH_MSG_NEWKEYS for strict KEX";
+ String expected3 =
+ "Reset incoming sequence number after receiving SSH_MSG_NEWKEYS for strict KEX";
+ checkLogs(expectedServerKex);
+ checkNoLogs(expectedClientKex);
+ checkNoLogs(expected1);
+ checkNoLogs(expected2);
+ checkNoLogs(expected3);
+ }
+
+ 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();
+ try {
+ assertTrue(actualJsch.isPresent(), () -> "JSch: " + expected);
+ } catch (AssertionError e) {
+ printInfo();
+ throw e;
+ }
+ }
+
+ private void checkNoLogs(String expected) {
+ Optional actualJsch = jschLogger.getAllLoggingEvents().stream()
+ .map(LoggingEvent::getFormattedMessage).filter(msg -> msg.matches(expected)).findFirst();
+ try {
+ assertFalse(actualJsch.isPresent(), () -> "JSch: " + 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/UserAuthIT.java b/src/test/java/com/jcraft/jsch/UserAuthIT.java
index a0e5fc11..774cae5f 100644
--- a/src/test/java/com/jcraft/jsch/UserAuthIT.java
+++ b/src/test/java/com/jcraft/jsch/UserAuthIT.java
@@ -14,6 +14,7 @@
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;
+import java.util.Locale;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.jupiter.api.AfterAll;
@@ -144,7 +145,8 @@ private JSch createRSAIdentity() throws Exception {
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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
+ String hostname =
+ String.format(Locale.ROOT, "[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort());
return new HostKey(hostname, Base64.getDecoder().decode(split[1]));
}
diff --git a/src/test/java/com/jcraft/jsch/jbcrypt/BCryptTest.java b/src/test/java/com/jcraft/jsch/jbcrypt/BCryptTest.java
index c4edcbdd..6f69f4c8 100644
--- a/src/test/java/com/jcraft/jsch/jbcrypt/BCryptTest.java
+++ b/src/test/java/com/jcraft/jsch/jbcrypt/BCryptTest.java
@@ -14,11 +14,11 @@
package com.jcraft.jsch.jbcrypt;
-import org.junit.jupiter.api.Test;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.*;
import java.util.Arrays;
-
-import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
/**
* JUnit unit tests for BCrypt routines
@@ -246,14 +246,14 @@ public BCryptPbkdfTV(byte[] pass, byte[] salt, int rounds, byte[] out) {
}
BCryptPbkdfTV[] bcrypt_pbkdf_test_vectors = new BCryptPbkdfTV[] {
- new BCryptPbkdfTV("password".getBytes(), "salt".getBytes(), 4,
+ new BCryptPbkdfTV("password".getBytes(UTF_8), "salt".getBytes(UTF_8), 4,
new byte[] {(byte) 0x5b, (byte) 0xbf, (byte) 0x0c, (byte) 0xc2, (byte) 0x93, (byte) 0x58,
(byte) 0x7f, (byte) 0x1c, (byte) 0x36, (byte) 0x35, (byte) 0x55, (byte) 0x5c,
(byte) 0x27, (byte) 0x79, (byte) 0x65, (byte) 0x98, (byte) 0xd4, (byte) 0x7e,
(byte) 0x57, (byte) 0x90, (byte) 0x71, (byte) 0xbf, (byte) 0x42, (byte) 0x7e,
(byte) 0x9d, (byte) 0x8f, (byte) 0xbe, (byte) 0x84, (byte) 0x2a, (byte) 0xba,
(byte) 0x34, (byte) 0xd9,}),
- new BCryptPbkdfTV("password".getBytes(), "salt".getBytes(), 8,
+ new BCryptPbkdfTV("password".getBytes(UTF_8), "salt".getBytes(UTF_8), 8,
new byte[] {(byte) 0xe1, (byte) 0x36, (byte) 0x7e, (byte) 0xc5, (byte) 0x15, (byte) 0x1a,
(byte) 0x33, (byte) 0xfa, (byte) 0xac, (byte) 0x4c, (byte) 0xc1, (byte) 0xc1,
(byte) 0x44, (byte) 0xcd, (byte) 0x23, (byte) 0xfa, (byte) 0x15, (byte) 0xd5,
@@ -265,7 +265,7 @@ public BCryptPbkdfTV(byte[] pass, byte[] salt, int rounds, byte[] out) {
(byte) 0xe7, (byte) 0x4b, (byte) 0xba, (byte) 0x51, (byte) 0x72, (byte) 0x3f,
(byte) 0xef, (byte) 0xa9, (byte) 0xf9, (byte) 0x47, (byte) 0x4d, (byte) 0x65,
(byte) 0x08, (byte) 0x84, (byte) 0x5e, (byte) 0x8d}),
- new BCryptPbkdfTV("password".getBytes(), "salt".getBytes(), 42,
+ new BCryptPbkdfTV("password".getBytes(UTF_8), "salt".getBytes(UTF_8), 42,
new byte[] {(byte) 0x83, (byte) 0x3c, (byte) 0xf0, (byte) 0xdc, (byte) 0xf5, (byte) 0x6d,
(byte) 0xb6, (byte) 0x56, (byte) 0x08, (byte) 0xe8, (byte) 0xf0, (byte) 0xdc,
(byte) 0x0c, (byte) 0xe8, (byte) 0x82, (byte) 0xbd}),};
diff --git a/src/test/java/com/jcraft/jsch/jzlib/DeflateInflateTest.java b/src/test/java/com/jcraft/jsch/jzlib/DeflateInflateTest.java
index 7cfdf795..3d2489d5 100644
--- a/src/test/java/com/jcraft/jsch/jzlib/DeflateInflateTest.java
+++ b/src/test/java/com/jcraft/jsch/jzlib/DeflateInflateTest.java
@@ -1,6 +1,7 @@
package com.jcraft.jsch.jzlib;
import static com.jcraft.jsch.jzlib.JZlib.*;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -90,7 +91,7 @@ public void testDeflaterAndInflaterCanDeflateAndInflateDataInLargeBuffer() {
@Test
public void testDeflaterAndInflaterCanDeflateAndInflateDataInSmallBuffer() {
- byte[] data = "hello, hello!".getBytes();
+ byte[] data = "hello, hello!".getBytes(UTF_8);
err = deflater.init(Z_DEFAULT_COMPRESSION);
assertEquals(Z_OK, err);
@@ -142,8 +143,8 @@ public void testDeflaterAndInflaterCanDeflateAndInflateDataInSmallBuffer() {
@Test
public void testDeflaterAndInflaterSupportDictionary() {
- byte[] hello = "hello".getBytes();
- byte[] dictionary = "hello, hello!".getBytes();
+ byte[] hello = "hello".getBytes(UTF_8);
+ byte[] dictionary = "hello, hello!".getBytes(UTF_8);
err = deflater.init(Z_DEFAULT_COMPRESSION);
assertEquals(Z_OK, err);
@@ -198,7 +199,7 @@ public void testDeflaterAndInflaterSupportDictionary() {
@Test
public void testDeflaterAndInflaterSupportSync() {
- byte[] hello = "hello".getBytes();
+ byte[] hello = "hello".getBytes(UTF_8);
err = deflater.init(Z_DEFAULT_COMPRESSION);
assertEquals(Z_OK, err);
@@ -244,12 +245,12 @@ public void testDeflaterAndInflaterSupportSync() {
byte[] actual = new byte[total_out];
System.arraycopy(uncompr, 0, actual, 0, total_out);
- assertEquals(new String(hello), "hel" + new String(actual));
+ assertEquals(new String(hello, UTF_8), "hel" + new String(actual, UTF_8));
}
@Test
public void testInflaterCanInflateGzipData() {
- byte[] hello = "foo".getBytes();
+ byte[] hello = "foo".getBytes(UTF_8);
byte[] data = {(byte) 0x1f, (byte) 0x8b, (byte) 0x08, (byte) 0x18, (byte) 0x08, (byte) 0xeb,
(byte) 0x7a, (byte) 0x0b, (byte) 0x00, (byte) 0x0b, (byte) 0x58, (byte) 0x00, (byte) 0x59,
(byte) 0x00, (byte) 0x4b, (byte) 0xcb, (byte) 0xcf, (byte) 0x07, (byte) 0x00, (byte) 0x21,
@@ -284,7 +285,7 @@ public void testInflaterCanInflateGzipData() {
@Test
public void testInflaterAndDeflaterCanSupportGzipData() {
- byte[] data = "hello, hello!".getBytes();
+ byte[] data = "hello, hello!".getBytes(UTF_8);
err = deflater.init(Z_DEFAULT_COMPRESSION, 15 + 16);
assertEquals(Z_OK, err);
diff --git a/src/test/java/com/jcraft/jsch/jzlib/DeflaterInflaterStreamTest.java b/src/test/java/com/jcraft/jsch/jzlib/DeflaterInflaterStreamTest.java
index 82c2f671..1e9b018d 100644
--- a/src/test/java/com/jcraft/jsch/jzlib/DeflaterInflaterStreamTest.java
+++ b/src/test/java/com/jcraft/jsch/jzlib/DeflaterInflaterStreamTest.java
@@ -1,6 +1,7 @@
package com.jcraft.jsch.jzlib;
import static com.jcraft.jsch.jzlib.Package.*;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -94,7 +95,7 @@ public void testDeflaterAndInflaterCanDeflateAndInflateNowrapDataWithMaxWbits()
Arrays.asList(randombuf(10240),
"{\"color\":2,\"id\":\"EvLd4UG.CXjnk35o1e8LrYYQfHu0h.d*SqVJPoqmzXM::Ly::Snaps::Store::Commit\"}"
- .getBytes())
+ .getBytes(UTF_8))
.forEach(uncheckedConsumer(data1 -> {
Deflater deflater = new Deflater(JZlib.Z_DEFAULT_COMPRESSION, JZlib.MAX_WBITS, true);
diff --git a/src/test/java/com/jcraft/jsch/jzlib/WrapperTypeTest.java b/src/test/java/com/jcraft/jsch/jzlib/WrapperTypeTest.java
index 91b24949..4ec42f9e 100644
--- a/src/test/java/com/jcraft/jsch/jzlib/WrapperTypeTest.java
+++ b/src/test/java/com/jcraft/jsch/jzlib/WrapperTypeTest.java
@@ -2,6 +2,7 @@
import static com.jcraft.jsch.jzlib.JZlib.*;
import static com.jcraft.jsch.jzlib.Package.*;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -17,7 +18,7 @@
import org.junit.jupiter.api.Test;
public class WrapperTypeTest {
- private final byte[] data = "hello, hello!".getBytes();
+ private final byte[] data = "hello, hello!".getBytes(UTF_8);
private final int comprLen = 40000;
private final int uncomprLen = comprLen;
@@ -92,7 +93,7 @@ public void testZStreamCanDetectDataTypeOfInput() {
c.good.forEach(w -> {
ZStream inflater = inflate(compr, uncompr, w);
int total_out = (int) inflater.total_out;
- assertEquals(new String(data), new String(uncompr, 0, total_out));
+ assertEquals(new String(data, UTF_8), new String(uncompr, 0, total_out, UTF_8));
});
c.bad.forEach(w -> {
@@ -129,7 +130,7 @@ public void testDeflaterCanSupportWbitsPlus32() {
assertEquals(Z_OK, err);
int total_out = (int) inflater.total_out;
- assertEquals(new String(data), new String(uncompr, 0, total_out));
+ assertEquals(new String(data, UTF_8), new String(uncompr, 0, total_out, UTF_8));
deflater = new Deflater();
err = deflater.init(Z_BEST_SPEED, DEF_WBITS + 16, 9);
@@ -156,7 +157,7 @@ public void testDeflaterCanSupportWbitsPlus32() {
assertEquals(Z_OK, err);
total_out = (int) inflater.total_out;
- assertEquals(new String(data), new String(uncompr, 0, total_out));
+ assertEquals(new String(data, UTF_8), new String(uncompr, 0, total_out, UTF_8));
}
private void deflate(ZStream deflater, byte[] data, byte[] compr) {
diff --git a/src/test/resources/docker/Dockerfile.ExtInfoInAuthIT b/src/test/resources/docker/Dockerfile.ExtInfoInAuthIT
new file mode 100644
index 00000000..06a3f9bb
--- /dev/null
+++ b/src/test/resources/docker/Dockerfile.ExtInfoInAuthIT
@@ -0,0 +1,36 @@
+FROM alpine:3.19
+RUN apk update && \
+ apk upgrade && \
+ apk add openssh && \
+ rm /var/cache/apk/* && \
+ addgroup -g 1000 rsa && \
+ adduser -Du 1000 -G rsa -Hh /rsa -s /bin/sh -g rsa rsa && \
+ mkdir -p /rsa/.ssh && \
+ chown -R rsa:rsa /rsa && \
+ chmod 700 /rsa /rsa/.ssh && \
+ passwd -u rsa && \
+ addgroup -g 1001 ecdsa && \
+ adduser -Du 1001 -G ecdsa -Hh /ecdsa -s /bin/sh -g ecdsa ecdsa && \
+ mkdir -p /ecdsa/.ssh && \
+ chown -R ecdsa:ecdsa /ecdsa && \
+ chmod 700 /ecdsa /ecdsa/.ssh && \
+ passwd -u ecdsa
+COPY ssh_host_rsa_key /etc/ssh/
+COPY ssh_host_rsa_key.pub /etc/ssh/
+COPY ssh_host_ecdsa256_key /etc/ssh/
+COPY ssh_host_ecdsa256_key.pub /etc/ssh/
+COPY ssh_host_ecdsa384_key /etc/ssh/
+COPY ssh_host_ecdsa384_key.pub /etc/ssh/
+COPY ssh_host_ecdsa521_key /etc/ssh/
+COPY ssh_host_ecdsa521_key.pub /etc/ssh/
+COPY ssh_host_ed25519_key /etc/ssh/
+COPY ssh_host_ed25519_key.pub /etc/ssh/
+COPY ssh_host_dsa_key /etc/ssh/
+COPY ssh_host_dsa_key.pub /etc/ssh/
+COPY sshd_config /etc/ssh/
+COPY authorized_keys /rsa/.ssh/
+COPY authorized_keys /ecdsa/.ssh/
+RUN chown rsa:rsa /rsa/.ssh/authorized_keys && \
+ chown ecdsa:ecdsa /ecdsa/.ssh/authorized_keys && \
+ chmod 600 /etc/ssh/ssh_*_key /rsa/.ssh/authorized_keys /ecdsa/.ssh/authorized_keys
+ENTRYPOINT ["/usr/sbin/sshd", "-D", "-e"]
diff --git a/src/test/resources/docker/Dockerfile.openssh96 b/src/test/resources/docker/Dockerfile.openssh96
new file mode 100644
index 00000000..474c9282
--- /dev/null
+++ b/src/test/resources/docker/Dockerfile.openssh96
@@ -0,0 +1,23 @@
+FROM alpine:3.19
+RUN apk update && \
+ apk upgrade && \
+ apk add openssh && \
+ rm /var/cache/apk/* && \
+ mkdir /root/.ssh && \
+ chmod 700 /root/.ssh
+COPY ssh_host_rsa_key /etc/ssh/
+COPY ssh_host_rsa_key.pub /etc/ssh/
+COPY ssh_host_ecdsa256_key /etc/ssh/
+COPY ssh_host_ecdsa256_key.pub /etc/ssh/
+COPY ssh_host_ecdsa384_key /etc/ssh/
+COPY ssh_host_ecdsa384_key.pub /etc/ssh/
+COPY ssh_host_ecdsa521_key /etc/ssh/
+COPY ssh_host_ecdsa521_key.pub /etc/ssh/
+COPY ssh_host_ed25519_key /etc/ssh/
+COPY ssh_host_ed25519_key.pub /etc/ssh/
+COPY ssh_host_dsa_key /etc/ssh/
+COPY ssh_host_dsa_key.pub /etc/ssh/
+COPY sshd_config /etc/ssh/
+COPY authorized_keys /root/.ssh/
+RUN chmod 600 /etc/ssh/ssh_*_key /root/.ssh/authorized_keys
+ENTRYPOINT ["/usr/sbin/sshd", "-D", "-e"]
diff --git a/src/test/resources/docker/sshd_config.ExtInfoInAuthIT b/src/test/resources/docker/sshd_config.ExtInfoInAuthIT
new file mode 100644
index 00000000..d63c7f83
--- /dev/null
+++ b/src/test/resources/docker/sshd_config.ExtInfoInAuthIT
@@ -0,0 +1,25 @@
+ChallengeResponseAuthentication no
+HostbasedAuthentication no
+PasswordAuthentication no
+PubkeyAuthentication yes
+AuthenticationMethods publickey
+PubkeyAcceptedAlgorithms ssh-ed25519
+UseDNS no
+PrintMotd no
+PermitRootLogin yes
+Subsystem sftp internal-sftp
+HostKey /etc/ssh/ssh_host_ecdsa256_key
+HostKey /etc/ssh/ssh_host_ecdsa384_key
+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
+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
+LogLevel DEBUG3
+Match User rsa
+ PubkeyAcceptedAlgorithms rsa-sha2-512,rsa-sha2-256,ssh-rsa
+Match User ecdsa
+ PubkeyAcceptedAlgorithms ecdsa-sha2-nistp521,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256
diff --git a/src/test/resources/docker/sshd_config.openssh96 b/src/test/resources/docker/sshd_config.openssh96
new file mode 100644
index 00000000..8d7bcba1
--- /dev/null
+++ b/src/test/resources/docker/sshd_config.openssh96
@@ -0,0 +1,21 @@
+ChallengeResponseAuthentication no
+HostbasedAuthentication no
+PasswordAuthentication no
+PubkeyAuthentication yes
+AuthenticationMethods publickey
+PubkeyAcceptedAlgorithms ecdsa-sha2-nistp521,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa,ssh-dss
+UseDNS no
+PrintMotd no
+PermitRootLogin yes
+Subsystem sftp internal-sftp
+HostKey /etc/ssh/ssh_host_ecdsa256_key
+HostKey /etc/ssh/ssh_host_ecdsa384_key
+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
+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
+LogLevel DEBUG3