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..ea970135 --- /dev/null +++ b/src/test/java/com/jcraft/jsch/SSHAgentIT.java @@ -0,0 +1,419 @@ +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(); + 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"]