Skip to content

Commit

Permalink
#457 add integration tests for new strict key exchange extension.
Browse files Browse the repository at this point in the history
  • Loading branch information
norrisjeremy committed Dec 19, 2023
1 parent 2374547 commit d7622c2
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 0 deletions.
270 changes: 270 additions & 0 deletions src/test/java/com/jcraft/jsch/StrictKexIT.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> 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);
}
}
23 changes: 23 additions & 0 deletions src/test/resources/docker/Dockerfile.openssh96
Original file line number Diff line number Diff line change
@@ -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"]
21 changes: 21 additions & 0 deletions src/test/resources/docker/sshd_config.openssh96
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
ChallengeResponseAuthentication no
HostbasedAuthentication no
PasswordAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
PubkeyAcceptedKeyTypes 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

0 comments on commit d7622c2

Please sign in to comment.