From 41d33dcc4af25cf666df5147ff4b204b571ddbd0 Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Mon, 6 Sep 2021 08:44:41 -0500 Subject: [PATCH 1/3] Allow usage of keys from ssh-agent that would otherwise not work because of missing support for the signature algorithm. --- .../com/jcraft/jsch/UserAuthPublicKey.java | 150 +++++++++--------- .../jcraft/jsch/OpenSSH74ServerSigAlgsIT.java | 6 +- .../java/com/jcraft/jsch/ServerSigAlgsIT.java | 8 +- 3 files changed, 79 insertions(+), 85 deletions(-) diff --git a/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java b/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java index 87660162..05282730 100644 --- a/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java +++ b/src/main/java/com/jcraft/jsch/UserAuthPublicKey.java @@ -29,7 +29,7 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING package com.jcraft.jsch; -import java.util.Vector; +import java.util.*; class UserAuthPublicKey extends UserAuth{ @@ -44,75 +44,70 @@ public boolean start(Session session) throws Exception{ return false; } - String pkmethods=session.getConfig("PubkeyAcceptedAlgorithms"); + String pkmethodstr=session.getConfig("PubkeyAcceptedAlgorithms"); if(JSch.getLogger().isEnabled(Logger.DEBUG)){ JSch.getLogger().log(Logger.DEBUG, - "Before pruning PubkeyAcceptedAlgorithms = " + pkmethods); + "PubkeyAcceptedAlgorithms = " + pkmethodstr); } - String[] not_available_pks = session.getUnavailableSignatures(); - if(not_available_pks!=null && not_available_pks.length>0){ - pkmethods=Util.diffString(pkmethods, not_available_pks); - if(pkmethods==null){ - throw new JSchException("There are not any available sig algorithm."); + String[] not_available_pka = session.getUnavailableSignatures(); + List not_available_pks=(not_available_pka!=null && not_available_pka.length>0 ? + Arrays.asList(not_available_pka) : + Collections.emptyList()); + if(!not_available_pks.isEmpty()){ + if(JSch.getLogger().isEnabled(Logger.DEBUG)){ + JSch.getLogger().log(Logger.DEBUG, + "Signature algorithms unavailable for non-agent identities = " + not_available_pks); } } - if(JSch.getLogger().isEnabled(Logger.DEBUG)){ - JSch.getLogger().log(Logger.DEBUG, - "After getUnavailableSignatures PubkeyAcceptedAlgorithms = " + pkmethods); - } - String[] pkmethoda=Util.split(pkmethods, ","); - if(pkmethoda.length==0){ + List pkmethods=Arrays.asList(Util.split(pkmethodstr, ",")); + if(pkmethods.isEmpty()){ return false; } String[] server_sig_algs=session.getServerSigAlgs(); if(server_sig_algs!=null && server_sig_algs.length>0){ - String _known=null; - String _unknown=null; - for(int i=0; i _known=new ArrayList<>(); + List _unknown=new ArrayList<>(); + for(String pkmethod : pkmethods){ boolean add=false; - for(int j=0; j identities, String[] pkmethoda) throws Exception{ + private boolean _start(Session session, List identities, List pkmethods, List not_available_pks) throws Exception{ if(session.auth_failures >= session.max_auth_tries){ return false; } - String rsamethods=null; - String nonrsamethods=null; - for(int i=0; i rsamethods=new ArrayList<>(); + List nonrsamethods=new ArrayList<>(); + for(String pkmethod : pkmethods){ + if(pkmethod.equals("ssh-rsa") || pkmethod.equals("rsa-sha2-256") || pkmethod.equals("rsa-sha2-512") || + pkmethod.equals("ssh-rsa-sha224@ssh.com") || pkmethod.equals("ssh-rsa-sha256@ssh.com") || + pkmethod.equals("ssh-rsa-sha384@ssh.com") || pkmethod.equals("ssh-rsa-sha512@ssh.com")){ + rsamethods.add(pkmethod); } else{ - if(nonrsamethods==null) nonrsamethods=pkmethoda[i]; - else nonrsamethods+=","+pkmethoda[i]; + nonrsamethods.add(pkmethod); } } - String[] rsamethoda=Util.split(rsamethods, ","); - String[] nonrsamethoda=Util.split(nonrsamethods, ","); byte[] _username=Util.str2byte(username); int command; iloop: - for(int i=0; i= session.max_auth_tries){ return false; } - Identity identity=identities.elementAt(i); - //System.err.println("UserAuthPublicKey: identity.isEncrypted()="+identity.isEncrypted()); decryptKey(session, identity); //System.err.println("UserAuthPublicKey: identity.isEncrypted()="+identity.isEncrypted()); - String ipkmethod=identity.getAlgName(); - String[] ipkmethoda=null; - if(ipkmethod.equals("ssh-rsa")){ - ipkmethoda=rsamethoda; + String _ipkmethod=identity.getAlgName(); + List ipkmethods=null; + if(_ipkmethod.equals("ssh-rsa")){ + ipkmethods=rsamethods; } - else if(nonrsamethoda!=null && nonrsamethoda.length>0){ - for(int j=0; j pkmethodsuccesses=null; if(pubkeyblob!=null){ command=SSH_MSG_USERAUTH_FAILURE; loop3: - for(int j=0; j0){ buf.putString(Util.str2byte("ssh-connection")); buf.putString(Util.str2byte("publickey")); buf.putByte((byte)0); - buf.putString(Util.str2byte(ipkmethoda[j])); + buf.putString(Util.str2byte(ipkmethod)); buf.putString(pubkeyblob); session.write(packet); @@ -218,15 +210,15 @@ else if(nonrsamethoda!=null && nonrsamethoda.length>0){ if(command==SSH_MSG_USERAUTH_PK_OK){ if(JSch.getLogger().isEnabled(Logger.DEBUG)){ JSch.getLogger().log(Logger.DEBUG, - ipkmethoda[j] + " preauth success"); + ipkmethod + " preauth success"); } - pkmethodsuccess=new String[]{ipkmethoda[j]}; + pkmethodsuccesses=Collections.singletonList(ipkmethod); break loop3; } else if(command==SSH_MSG_USERAUTH_FAILURE){ if(JSch.getLogger().isEnabled(Logger.DEBUG)){ JSch.getLogger().log(Logger.DEBUG, - ipkmethoda[j] + " preauth failure"); + ipkmethod + " preauth failure"); } continue loop3; } @@ -245,7 +237,7 @@ else if(command==SSH_MSG_USERAUTH_BANNER){ //throw new JSchException("USERAUTH fail ("+command+")"); if(JSch.getLogger().isEnabled(Logger.DEBUG)){ JSch.getLogger().log(Logger.DEBUG, - ipkmethoda[j] + " preauth failure command (" + command + ")"); + ipkmethod + " preauth failure command (" + command + ")"); } continue loop3; } @@ -265,10 +257,18 @@ else if(command==SSH_MSG_USERAUTH_BANNER){ //System.err.println("UserAuthPublicKey: pubkeyblob="+pubkeyblob); if(pubkeyblob==null) continue; - if(pkmethodsuccess==null) pkmethodsuccess=ipkmethoda; + if(pkmethodsuccesses==null) pkmethodsuccesses=ipkmethods; loop4: - for(int j=0; j Date: Mon, 6 Sep 2021 12:08:58 -0500 Subject: [PATCH 2/3] Add integration tests for ssh-agent support. --- src/test/java/com/jcraft/jsch/SSHAgentIT.java | 420 ++++++++++++++++++ src/test/resources/docker/Dockerfile.sshagent | 14 + 2 files changed, 434 insertions(+) create mode 100644 src/test/java/com/jcraft/jsch/SSHAgentIT.java create mode 100644 src/test/resources/docker/Dockerfile.sshagent diff --git a/src/test/java/com/jcraft/jsch/SSHAgentIT.java b/src/test/java/com/jcraft/jsch/SSHAgentIT.java new file mode 100644 index 00000000..2a13f4e4 --- /dev/null +++ b/src/test/java/com/jcraft/jsch/SSHAgentIT.java @@ -0,0 +1,420 @@ +package com.jcraft.jsch; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.codec.binary.Base64.decodeBase64; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.condition.JRE.JAVA_16; +import static org.junit.jupiter.api.condition.OS.LINUX; +import static org.testcontainers.containers.BindMode.READ_WRITE; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import com.sun.jna.platform.unix.LibC; +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.List; +import java.util.Random; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.LoggerFactory; +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; + +@EnabledOnOs(LINUX) +@Testcontainers +public class SSHAgentIT { + + private static final int timeout = 2000; + private static final DigestUtils sha256sum = new DigestUtils(DigestUtils.getSha256Digest()); + private static final ListAppender jschAppender = getListAppender(JSch.class); + private static final ListAppender sshdAppender = getListAppender(SSHAgentIT.class); + private static final ListAppender sshAgentAppender = getListAppender(AgentProxy.class); + @TempDir public static Path tmpDir; + private static String testuid; + private static String testgid; + private static Path sshAgentSock; + + private Path in; + private Path out; + private String hash; + private Slf4jLogConsumer sshdLogConsumer; + private Slf4jLogConsumer sshAgentLogConsumer; + + @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); + + @Container + public GenericContainer sshAgent = + new GenericContainer<>( + new ImageFromDockerfile() + .withBuildArg("testuid", testuid) + .withBuildArg("testgid", testgid) + .withFileFromClasspath("Dockerfile", "docker/Dockerfile.sshagent")) + .withFileSystemBind(sshAgentSock.getParent().toString(), "/testuser", READ_WRITE); + + @BeforeAll + public static void beforeAll() throws IOException { + JSch.setLogger(Slf4jLogger.getInstance()); + LibC libc = LibC.INSTANCE; + testuid = Integer.toString(libc.getuid()); + testgid = Integer.toString(libc.getgid()); + Path temp = Files.createTempDirectory(tmpDir, "sshagent"); + sshAgentSock = temp.resolve("sock"); + } + + @BeforeEach + public void beforeEach() throws IOException { + if (sshdLogConsumer == null) { + sshdLogConsumer = new Slf4jLogConsumer(LoggerFactory.getLogger(SSHAgentIT.class)); + sshd.followOutput(sshdLogConsumer); + } + + if (sshAgentLogConsumer == null) { + sshAgentLogConsumer = new Slf4jLogConsumer(LoggerFactory.getLogger(AgentProxy.class)); + sshAgent.followOutput(sshAgentLogConsumer); + } + + Path temp = Files.createTempDirectory(tmpDir, "sshd"); + in = temp.resolve("in"); + out = temp.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); + + jschAppender.list.clear(); + sshdAppender.list.clear(); + sshAgentAppender.list.clear(); + jschAppender.start(); + sshdAppender.start(); + sshAgentAppender.start(); + } + + @AfterEach + public void afterEach() { + jschAppender.stop(); + sshdAppender.stop(); + sshAgentAppender.stop(); + jschAppender.list.clear(); + sshdAppender.list.clear(); + sshAgentAppender.list.clear(); + + try { + Files.deleteIfExists(sshAgentSock); + } catch (IOException ignore) { + } + + try { + Files.deleteIfExists(out); + } catch (IOException ignore) { + } + + try { + Files.deleteIfExists(in); + } catch (IOException ignore) { + } + + try { + Files.deleteIfExists(in.getParent()); + } catch (IOException ignore) { + } + } + + @AfterAll + public static void afterAll() { + try { + Files.deleteIfExists(sshAgentSock.getParent()); + } catch (IOException ignore) { + } + } + + @Test + public void testEd25519JUnixSocketFactory() throws Exception { + JSch ssh = createEd25519Identity(new JUnixSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", "ssh-ed25519"); + doSftp(session, true); + } + + @Test + public void testECDSA521JUnixSocketFactory() throws Exception { + JSch ssh = createECDSA521Identity(new JUnixSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", "ecdsa-sha2-nistp521"); + doSftp(session, true); + } + + @Test + public void testECDSA384JUnixSocketFactory() throws Exception { + JSch ssh = createECDSA384Identity(new JUnixSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", "ecdsa-sha2-nistp384"); + doSftp(session, true); + } + + @Test + public void testECDSA256JUnixSocketFactory() throws Exception { + JSch ssh = createECDSA256Identity(new JUnixSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", "ecdsa-sha2-nistp256"); + doSftp(session, true); + } + + @ParameterizedTest + @ValueSource(strings = {"rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"}) + public void testRSAJUnixSocketFactory(String keyType) throws Exception { + JSch ssh = createRSAIdentity(new JUnixSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", keyType); + doSftp(session, true); + } + + @Test + public void testDSAJUnixSocketFactory() throws Exception { + JSch ssh = createDSAIdentity(new JUnixSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", "ssh-dss"); + doSftp(session, true); + } + + @Test + @EnabledForJreRange(min = JAVA_16) + public void testEd25519UnixDomainSocketFactory() throws Exception { + JSch ssh = createEd25519Identity(new UnixDomainSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", "ssh-ed25519"); + doSftp(session, true); + } + + @Test + @EnabledForJreRange(min = JAVA_16) + public void testECDSA521UnixDomainSocketFactory() throws Exception { + JSch ssh = createECDSA521Identity(new UnixDomainSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", "ecdsa-sha2-nistp521"); + doSftp(session, true); + } + + @Test + @EnabledForJreRange(min = JAVA_16) + public void testECDSA384UnixDomainSocketFactory() throws Exception { + JSch ssh = createECDSA384Identity(new UnixDomainSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", "ecdsa-sha2-nistp384"); + doSftp(session, true); + } + + @Test + @EnabledForJreRange(min = JAVA_16) + public void testECDSA256UnixDomainSocketFactory() throws Exception { + JSch ssh = createECDSA256Identity(new UnixDomainSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", "ecdsa-sha2-nistp256"); + doSftp(session, true); + } + + @ParameterizedTest + @ValueSource(strings = {"rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"}) + @EnabledForJreRange(min = JAVA_16) + public void testRSAUnixDomainSocketFactory(String keyType) throws Exception { + JSch ssh = createRSAIdentity(new UnixDomainSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", keyType); + doSftp(session, true); + } + + @Test + @EnabledForJreRange(min = JAVA_16) + public void testDSAUnixDomainSocketFactory() throws Exception { + JSch ssh = createDSAIdentity(new UnixDomainSocketFactory()); + Session session = createSession(ssh); + session.setConfig("PubkeyAcceptedAlgorithms", "ssh-dss"); + doSftp(session, true); + } + + private JSch createRSAIdentity(USocketFactory factory) throws Exception { + IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock)); + assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty"); + + HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub")); + JSch ssh = new JSch(); + ssh.setIdentityRepository(ir); + ssh.addIdentity(getResourceFile("docker/id_rsa"), getResourceFile("docker/id_rsa.pub"), null); + assertEquals(1, ir.getIdentities().size()); + ssh.getHostKeyRepository().add(hostKey, null); + return ssh; + } + + private JSch createECDSA256Identity(USocketFactory factory) throws Exception { + IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock)); + assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty"); + + HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub")); + JSch ssh = new JSch(); + ssh.setIdentityRepository(ir); + ssh.addIdentity( + getResourceFile("docker/id_ecdsa256"), getResourceFile("docker/id_ecdsa256.pub"), null); + ssh.getHostKeyRepository().add(hostKey, null); + return ssh; + } + + private JSch createECDSA384Identity(USocketFactory factory) throws Exception { + IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock)); + assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty"); + + HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub")); + JSch ssh = new JSch(); + ssh.setIdentityRepository(ir); + ssh.addIdentity( + getResourceFile("docker/id_ecdsa384"), getResourceFile("docker/id_ecdsa384.pub"), null); + assertEquals(1, ir.getIdentities().size()); + ssh.getHostKeyRepository().add(hostKey, null); + return ssh; + } + + private JSch createECDSA521Identity(USocketFactory factory) throws Exception { + IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock)); + assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty"); + + HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub")); + JSch ssh = new JSch(); + ssh.setIdentityRepository(ir); + ssh.addIdentity( + getResourceFile("docker/id_ecdsa521"), getResourceFile("docker/id_ecdsa521.pub"), null); + assertEquals(1, ir.getIdentities().size()); + ssh.getHostKeyRepository().add(hostKey, null); + return ssh; + } + + private JSch createDSAIdentity(USocketFactory factory) throws Exception { + IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock)); + assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty"); + + HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub")); + JSch ssh = new JSch(); + ssh.setIdentityRepository(ir); + ssh.addIdentity(getResourceFile("docker/id_dsa"), getResourceFile("docker/id_dsa.pub"), null); + assertEquals(1, ir.getIdentities().size()); + ssh.getHostKeyRepository().add(hostKey, null); + return ssh; + } + + private JSch createEd25519Identity(USocketFactory factory) throws Exception { + IdentityRepository ir = new AgentIdentityRepository(new SSHAgentConnector(factory, sshAgentSock)); + assertTrue(ir.getIdentities().isEmpty(), "ssh-agent empty"); + + HostKey hostKey = readHostKey(getResourceFile("docker/ssh_host_rsa_key.pub")); + JSch ssh = new JSch(); + ssh.setIdentityRepository(ir); + ssh.addIdentity( + getResourceFile("docker/id_ed25519"), getResourceFile("docker/id_ed25519.pub"), null); + assertEquals(1, ir.getIdentities().size()); + 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("[%s]:%d", sshd.getHost(), sshd.getFirstMappedPort()); + return new HostKey(hostname, decodeBase64(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(); + jschAppender.stop(); + sshdAppender.stop(); + sshAgentAppender.stop(); + } catch (Exception e) { + if (debugException) { + printInfo(); + } + throw e; + } + + assertEquals(1024L * 100L, Files.size(out)); + assertEquals(hash, sha256sum.digestAsHex(out)); + } + + private static void printInfo() { + jschAppender.stop(); + sshdAppender.stop(); + sshAgentAppender.stop(); + jschAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println); + sshdAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println); + sshAgentAppender.list.stream().map(ILoggingEvent::getFormattedMessage).forEach(System.out::println); + System.out.println(""); + System.out.println(""); + System.out.println(""); + } + + private String getResourceFile(String fileName) { + return ResourceUtil.getResourceFile(getClass(), fileName); + } + + private static ListAppender getListAppender(Class clazz) { + Logger logger = (Logger) LoggerFactory.getLogger(clazz); + ListAppender listAppender = new ListAppender2<>(); + logger.addAppender(listAppender); + return listAppender; + } +} diff --git a/src/test/resources/docker/Dockerfile.sshagent b/src/test/resources/docker/Dockerfile.sshagent new file mode 100644 index 00000000..f695ab3f --- /dev/null +++ b/src/test/resources/docker/Dockerfile.sshagent @@ -0,0 +1,14 @@ +FROM alpine:3.7 +ARG testuid +ARG testgid +RUN apk update && \ + apk upgrade && \ + apk add openssh su-exec && \ + rm /var/cache/apk/* && \ + addgroup -g $testgid testuser && \ + adduser -Du $testuid -G testuser -Hh /testuser -s /bin/sh -g testuser testuser && \ + mkdir /testuser && \ + chown testuser:testuser /testuser && \ + chmod 700 /testuser && \ + passwd -u testuser +ENTRYPOINT ["/sbin/su-exec", "testuser:testuser", "/usr/bin/ssh-agent", "-d", "-a", "/testuser/sock"] From a249d10d76d3465675e61eab46d882fa5cad90fa Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Mon, 6 Sep 2021 21:43:31 -0500 Subject: [PATCH 3/3] Add support for various algorithms with older Java releases by using Bouncy Castle: - ssh-ed25519 - ssh-ed448 - curve25519-sha256 - curve25519-sha256@libssh.org - curve448-sha512 - chacha20-poly1305@openssh.com --- Readme.md | 8 + pom.xml | 7 + src/main/java/com/jcraft/jsch/DHXEC.java | 2 +- src/main/java/com/jcraft/jsch/JSch.java | 26 +++- .../java/com/jcraft/jsch/JavaVersion.java | 8 + .../java/com/jcraft/jsch/KeyExchange.java | 31 ++-- .../java/com/jcraft/jsch/KeyPairEdDSA.java | 6 +- src/main/java/com/jcraft/jsch/Session.java | 15 +- .../com/jcraft/jsch/bc/ChaCha20Poly1305.java | 138 ++++++++++++++++++ .../com/jcraft/jsch/bc/KeyPairGenEdDSA.java | 64 ++++++++ .../com/jcraft/jsch/bc/SignatureEd25519.java | 47 ++++++ .../com/jcraft/jsch/bc/SignatureEd448.java | 47 ++++++ .../com/jcraft/jsch/bc/SignatureEdDSA.java | 114 +++++++++++++++ src/main/java/com/jcraft/jsch/bc/XDH.java | 94 ++++++++++++ .../com/jcraft/jsch/jce/ChaCha20Poly1305.java | 1 - .../com/jcraft/jsch/jce/KeyPairGenXEC.java | 1 - .../java9/com/jcraft/jsch/JavaVersion.java | 8 + src/main/java9/module-info.java | 1 + .../java/com/jcraft/jsch/Algorithms2IT.java | 42 ++++++ .../java/com/jcraft/jsch/AlgorithmsIT.java | 69 +++++++++ 20 files changed, 696 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/jcraft/jsch/JavaVersion.java create mode 100644 src/main/java/com/jcraft/jsch/bc/ChaCha20Poly1305.java create mode 100644 src/main/java/com/jcraft/jsch/bc/KeyPairGenEdDSA.java create mode 100644 src/main/java/com/jcraft/jsch/bc/SignatureEd25519.java create mode 100644 src/main/java/com/jcraft/jsch/bc/SignatureEd448.java create mode 100644 src/main/java/com/jcraft/jsch/bc/SignatureEdDSA.java create mode 100644 src/main/java/com/jcraft/jsch/bc/XDH.java create mode 100644 src/main/java9/com/jcraft/jsch/JavaVersion.java diff --git a/Readme.md b/Readme.md index abc6269a..39035536 100644 --- a/Readme.md +++ b/Readme.md @@ -64,6 +64,7 @@ As I explained in a [blog post](http://www.matez.de/index.php/2020/06/22/the-fut * This library is a Multi-Release-jar, which means that you can only use certain features when a more recent Java version is used. * In order to use ssh-ed25519 & ssh-ed448, you must use at least Java 15. * In order to use curve25519-sha256, curve448-sha512 & chacha20-poly1305@openssh.com, you must use at least Java 11. + * As of the [0.1.66](https://github.com/mwiede/jsch/releases/tag/jsch-0.1.66) release, these algorithms can now be used with older Java releases if [Bouncy Castle](https://www.bouncycastle.org/) (bcprov-jdk15on) is added to the classpath. ## Changes since fork: * [0.1.66](https://github.com/mwiede/jsch/releases/tag/jsch-0.1.66) @@ -99,6 +100,13 @@ As I explained in a [blog post](http://www.matez.de/index.php/2020/06/22/the-fut * See `examples/JSchWithAgentProxy.java` for simple example * ssh-agent support requires either [Java 16's JEP 380](https://openjdk.java.net/jeps/380) or the addition of [junixsocket](https://github.com/kohlschutter/junixsocket) to classpath * Pageant support is untested & requires the addition of [JNA](https://github.com/java-native-access/jna) to classpath + * Added support for the following algorithms with older Java releases by using [Bouncy Castle](https://www.bouncycastle.org/): + * ssh-ed25519 + * ssh-ed448 + * curve25519-sha256 + * curve25519-sha256@libssh.org + * curve448-sha512 + * chacha20-poly1305@openssh.com * [0.1.65](https://github.com/mwiede/jsch/releases/tag/jsch-0.1.65) * Added system properties to allow manipulation of various crypto algorithms used by default * Integrated JZlib, allowing use of zlib@openssh.com & zlib compressions without the need to provide the JZlib jar-file diff --git a/pom.xml b/pom.xml index 19812b80..4992383c 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,12 @@ 5.9.0 + + org.bouncycastle + bcprov-jdk15on + 1.69 + true + com.kohlschutter.junixsocket junixsocket-common @@ -335,6 +341,7 @@ 0.8.7 + com/jcraft/jsch/JavaVersion.class com/jcraft/jsch/UnixDomainSocketFactory.class diff --git a/src/main/java/com/jcraft/jsch/DHXEC.java b/src/main/java/com/jcraft/jsch/DHXEC.java index 4bbeeeac..b1100207 100644 --- a/src/main/java/com/jcraft/jsch/DHXEC.java +++ b/src/main/java/com/jcraft/jsch/DHXEC.java @@ -89,7 +89,7 @@ public void init(Session session, Q_C = xdh.getQ(); buf.putString(Q_C); } - catch(Exception e){ + catch(Exception | NoClassDefFoundError e){ throw new JSchException(e.toString(), e); } diff --git a/src/main/java/com/jcraft/jsch/JSch.java b/src/main/java/com/jcraft/jsch/JSch.java index 6a83e70d..e6f6569e 100644 --- a/src/main/java/com/jcraft/jsch/JSch.java +++ b/src/main/java/com/jcraft/jsch/JSch.java @@ -106,13 +106,9 @@ public class JSch{ config.put("ecdh-sha2-nistp", "com.jcraft.jsch.jce.ECDHN"); - config.put("ssh-ed25519", "com.jcraft.jsch.jce.SignatureEd25519"); - config.put("ssh-ed448", "com.jcraft.jsch.jce.SignatureEd448"); - config.put("curve25519-sha256", "com.jcraft.jsch.DH25519"); config.put("curve25519-sha256@libssh.org", "com.jcraft.jsch.DH25519"); config.put("curve448-sha512", "com.jcraft.jsch.DH448"); - config.put("xdh", "com.jcraft.jsch.jce.XDH"); config.put("dh", "com.jcraft.jsch.jce.DH"); config.put("3des-cbc", "com.jcraft.jsch.jce.TripleDESCBC"); @@ -156,12 +152,10 @@ public class JSch{ config.put("keypairgen.dsa", "com.jcraft.jsch.jce.KeyPairGenDSA"); config.put("keypairgen.rsa", "com.jcraft.jsch.jce.KeyPairGenRSA"); config.put("keypairgen.ecdsa", "com.jcraft.jsch.jce.KeyPairGenECDSA"); - config.put("keypairgen.eddsa", "com.jcraft.jsch.jce.KeyPairGenEdDSA"); config.put("random", "com.jcraft.jsch.jce.Random"); config.put("none", "com.jcraft.jsch.CipherNone"); - config.put("chacha20-poly1305@openssh.com", "com.jcraft.jsch.jce.ChaCha20Poly1305"); config.put("aes128-gcm@openssh.com", "com.jcraft.jsch.jce.AES128GCM"); config.put("aes256-gcm@openssh.com", "com.jcraft.jsch.jce.AES256GCM"); @@ -189,6 +183,26 @@ public class JSch{ config.put("pbkdf", "com.jcraft.jsch.jce.PBKDF"); + if(JavaVersion.getVersion()>=11){ + config.put("chacha20-poly1305@openssh.com", "com.jcraft.jsch.jce.ChaCha20Poly1305"); + config.put("xdh", "com.jcraft.jsch.jce.XDH"); + } + else{ + config.put("chacha20-poly1305@openssh.com", "com.jcraft.jsch.bc.ChaCha20Poly1305"); + config.put("xdh", "com.jcraft.jsch.bc.XDH"); + } + + if(JavaVersion.getVersion()>=15){ + config.put("keypairgen.eddsa", "com.jcraft.jsch.jce.KeyPairGenEdDSA"); + config.put("ssh-ed25519", "com.jcraft.jsch.jce.SignatureEd25519"); + config.put("ssh-ed448", "com.jcraft.jsch.jce.SignatureEd448"); + } + else{ + config.put("keypairgen.eddsa", "com.jcraft.jsch.bc.KeyPairGenEdDSA"); + config.put("ssh-ed25519", "com.jcraft.jsch.bc.SignatureEd25519"); + config.put("ssh-ed448", "com.jcraft.jsch.bc.SignatureEd448"); + } + config.put("StrictHostKeyChecking", "ask"); config.put("HashKnownHosts", "no"); diff --git a/src/main/java/com/jcraft/jsch/JavaVersion.java b/src/main/java/com/jcraft/jsch/JavaVersion.java new file mode 100644 index 00000000..b6184b14 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/JavaVersion.java @@ -0,0 +1,8 @@ +package com.jcraft.jsch; + +final class JavaVersion { + + static int getVersion() { + return 8; + } +} diff --git a/src/main/java/com/jcraft/jsch/KeyExchange.java b/src/main/java/com/jcraft/jsch/KeyExchange.java index cd2e86f9..cb611909 100644 --- a/src/main/java/com/jcraft/jsch/KeyExchange.java +++ b/src/main/java/com/jcraft/jsch/KeyExchange.java @@ -143,18 +143,25 @@ else if(guess[i]==null){ } } - Class _s2cclazz=Class.forName(session.getConfig(guess[PROPOSAL_ENC_ALGS_STOC])); - Cipher _s2ccipher=(Cipher)(_s2cclazz.getDeclaredConstructor().newInstance()); - boolean _s2cAEAD=_s2ccipher.isAEAD(); - if(_s2cAEAD){ - guess[PROPOSAL_MAC_ALGS_STOC]=null; - } + boolean _s2cAEAD=false; + boolean _c2sAEAD=false; + try{ + Class _s2cclazz=Class.forName(session.getConfig(guess[PROPOSAL_ENC_ALGS_STOC])); + Cipher _s2ccipher=(Cipher)(_s2cclazz.getDeclaredConstructor().newInstance()); + _s2cAEAD=_s2ccipher.isAEAD(); + if(_s2cAEAD){ + guess[PROPOSAL_MAC_ALGS_STOC]=null; + } - Class _c2sclazz=Class.forName(session.getConfig(guess[PROPOSAL_ENC_ALGS_CTOS])); - Cipher _c2scipher=(Cipher)(_c2sclazz.getDeclaredConstructor().newInstance()); - boolean _c2sAEAD=_c2scipher.isAEAD(); - if(_c2sAEAD){ - guess[PROPOSAL_MAC_ALGS_CTOS]=null; + Class _c2sclazz=Class.forName(session.getConfig(guess[PROPOSAL_ENC_ALGS_CTOS])); + Cipher _c2scipher=(Cipher)(_c2sclazz.getDeclaredConstructor().newInstance()); + _c2sAEAD=_c2scipher.isAEAD(); + if(_c2sAEAD){ + guess[PROPOSAL_MAC_ALGS_CTOS]=null; + } + } + catch(Exception | NoClassDefFoundError e){ + throw new JSchException(e.toString(), e); } if(JSch.getLogger().isEnabled(Logger.INFO)){ @@ -361,7 +368,7 @@ else if(alg.equals("ssh-ed25519") || sig=(SignatureEdDSA)(c.getDeclaredConstructor().newInstance()); sig.init(); } - catch(Exception e){ + catch(Exception | NoClassDefFoundError e){ System.err.println(e); } diff --git a/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java b/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java index 585a5be5..92d6a43f 100644 --- a/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java +++ b/src/main/java/com/jcraft/jsch/KeyPairEdDSA.java @@ -57,7 +57,7 @@ void generate(int key_size) throws JSchException{ keypairgen=null; } - catch(Exception e){ + catch(Exception | NoClassDefFoundError e){ //System.err.println("KeyPairEdDSA: "+e); throw new JSchException(e.toString(), e); } @@ -134,7 +134,7 @@ public byte[] getSignature(byte[] data, String alg){ tmp[1] = sig; return Buffer.fromBytes(tmp).buffer; } - catch(Exception e){ + catch(Exception | NoClassDefFoundError e){ } return null; } @@ -160,7 +160,7 @@ public Signature getVerifier(String alg){ eddsa.setPubKey(pub_array); return eddsa; } - catch(Exception e){ + catch(Exception | NoClassDefFoundError e){ } return null; } diff --git a/src/main/java/com/jcraft/jsch/Session.java b/src/main/java/com/jcraft/jsch/Session.java index 69e1bf34..80d807a7 100644 --- a/src/main/java/com/jcraft/jsch/Session.java +++ b/src/main/java/com/jcraft/jsch/Session.java @@ -610,7 +610,7 @@ private KeyExchange receive_kexinit(Buffer buf) throws Exception { Class c=Class.forName(getConfig(guess[KeyExchange.PROPOSAL_KEX_ALGS])); kex=(KeyExchange)(c.getDeclaredConstructor().newInstance()); } - catch(Exception e){ + catch(Exception | NoClassDefFoundError e){ throw new JSchException(e.toString(), e); } @@ -1528,7 +1528,7 @@ private void updateKeys(KeyExchange kex) throws Exception{ method=guess[KeyExchange.PROPOSAL_COMP_ALGS_STOC]; initInflater(method); } - catch(Exception e){ + catch(Exception | NoClassDefFoundError e){ if(e instanceof JSchException) throw e; throw new JSchException(e.toString(), e); @@ -2574,9 +2574,6 @@ private void initDeflater(String method) throws JSchException{ catch(Exception ee){ } deflater.init(Compression.DEFLATER, level); } - catch(NoClassDefFoundError ee){ - throw new JSchException(ee.toString(), ee); - } catch(Exception ee){ throw new JSchException(ee.toString(), ee); //System.err.println(foo+" isn't accessible."); @@ -2855,7 +2852,7 @@ static boolean checkCipher(String cipher){ new byte[_c.getIVSize()]); return true; } - catch(Exception e){ + catch(Exception | NoClassDefFoundError e){ return false; } } @@ -2904,7 +2901,7 @@ static boolean checkMac(String mac){ _c.init(new byte[_c.getBlockSize()]); return true; } - catch(Exception e){ + catch(Exception | NoClassDefFoundError e){ return false; } } @@ -2947,7 +2944,7 @@ static boolean checkKex(Session s, String kex){ _c.init(s ,null, null, null, null); return true; } - catch(Exception e){ return false; } + catch(Exception | NoClassDefFoundError e){ return false; } } private String[] checkSignatures(String sigs){ @@ -2967,7 +2964,7 @@ private String[] checkSignatures(String sigs){ final Signature sig=(Signature)(c.getDeclaredConstructor().newInstance()); sig.init(); } - catch(Exception e){ + catch(Exception | NoClassDefFoundError e){ result.addElement(_sigs[i]); } } diff --git a/src/main/java/com/jcraft/jsch/bc/ChaCha20Poly1305.java b/src/main/java/com/jcraft/jsch/bc/ChaCha20Poly1305.java new file mode 100644 index 00000000..8ab84dc9 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/bc/ChaCha20Poly1305.java @@ -0,0 +1,138 @@ +/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +/* +Copyright (c) 2008-2018 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package com.jcraft.jsch.bc; + +import com.jcraft.jsch.Cipher; +import com.jcraft.jsch.openjax.Poly1305; +import java.nio.ByteBuffer; +import javax.crypto.AEADBadTagException; +import org.bouncycastle.crypto.engines.ChaChaEngine; +import org.bouncycastle.crypto.params.*; + +public class ChaCha20Poly1305 implements Cipher{ + //Actually the block size, not IV size + private static final int ivsize=8; + //Actually the key size, not block size + private static final int bsize=64; + private static final int tagsize=16; + private ChaChaEngine header_cipher; + private ChaChaEngine main_cipher; + private KeyParameter K_1_spec; + private KeyParameter K_2_spec; + private int mode; + private Poly1305 poly1305; + @Override + public int getIVSize(){return ivsize;} + @Override + public int getBlockSize(){return bsize;} + @Override + public int getTagSize(){return tagsize;} + @Override + public void init(int mode, byte[] key, byte[] iv) throws Exception{ + byte[] tmp; + if(key.length>bsize){ + tmp=new byte[bsize]; + System.arraycopy(key, 0, tmp, 0, tmp.length); + key=tmp; + } + byte[] K_1=new byte[bsize/2]; + byte[] K_2=new byte[bsize/2]; + System.arraycopy(key, bsize/2, K_1, 0, bsize/2); + System.arraycopy(key, 0, K_2, 0, bsize/2); + this.mode=mode; + try{ + K_1_spec=new KeyParameter(K_1); + K_2_spec=new KeyParameter(K_2); + header_cipher=new ChaChaEngine(); + main_cipher=new ChaChaEngine(); + } + catch(Exception e){ + header_cipher=null; + main_cipher=null; + K_1_spec=null; + K_2_spec=null; + throw e; + } + } + @Override + public void update(int foo) throws Exception{ + ByteBuffer nonce=ByteBuffer.allocate(8); + nonce.putLong(0, foo); + header_cipher.init(this.mode==ENCRYPT_MODE, new ParametersWithIV(K_1_spec, nonce.array())); + main_cipher.init(this.mode==ENCRYPT_MODE, new ParametersWithIV(K_2_spec, nonce.array())); + // Trying to reinit the cipher again with same nonce results in InvalidKeyException + // So just read entire first 64-byte block, which should increment global counter from 0->1 + byte[] poly_key = new byte[32]; + byte[] discard = new byte[32]; + main_cipher.processBytes(poly_key, 0, 32, poly_key, 0); + main_cipher.processBytes(discard, 0, 32, discard, 0); + poly1305 = new Poly1305(poly_key); + } + @Override + public void update(byte[] foo, int s1, int len, byte[] bar, int s2) throws Exception{ + header_cipher.processBytes(foo, s1, len, bar, s2); + } + @Override + public void updateAAD(byte[] foo, int s1, int len) throws Exception{ + } + @Override + public void doFinal(byte[] foo, int s1, int len, byte[] bar, int s2) throws Exception{ + if(this.mode==DECRYPT_MODE){ + byte[] actual_tag = new byte[tagsize]; + System.arraycopy(foo, len, actual_tag, 0, tagsize); + byte[] expected_tag = new byte[tagsize]; + poly1305.update(foo, s1, len).finish(expected_tag, 0); + if(!arraysequals(actual_tag, expected_tag)){ + throw new AEADBadTagException("Tag mismatch"); + } + } + + main_cipher.processBytes(foo, s1+4, len-4, bar, s2+4); + + if(this.mode==ENCRYPT_MODE){ + poly1305.update(bar, s2, len).finish(bar, len); + } + } + @Override + public boolean isCBC(){return false; } + @Override + public boolean isAEAD(){return true; } + @Override + public boolean isChaCha20(){return true; } + + private static boolean arraysequals(byte[] a, byte[] b){ + if(a.length!=b.length) return false; + int res=0; + for(int i=0; iclient cipher: %s.*", cipher); + String expectedC2S = String.format("kex: client->server cipher: %s.*", cipher); + checkLogs(expectedS2C); + checkLogs(expectedC2S); + } + + @ParameterizedTest + @CsvSource( + value = { + "chacha20-poly1305@openssh.com,none", + "chacha20-poly1305@openssh.com,zlib@openssh.com", "aes256-gcm@openssh.com,none", "aes256-gcm@openssh.com,zlib@openssh.com", "aes128-gcm@openssh.com,none",