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 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 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