Skip to content

Commit

Permalink
GH-445: strict KEX interoperability tests
Browse files Browse the repository at this point in the history
Run an Apache MINA sshd client against OpenSSH servers that do have or
do not have strict KEX.
  • Loading branch information
tomaswolf committed Jan 5, 2024
1 parent 315739e commit 7b2c781
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.sshd.common.kex.extension;

import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.charset.StandardCharsets;

import org.apache.sshd.client.ClientFactoryManager;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ChannelShell;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.session.ClientSessionImpl;
import org.apache.sshd.client.session.SessionFactory;
import org.apache.sshd.common.channel.StreamingChannel;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
import org.apache.sshd.util.test.BaseTestSupport;
import org.apache.sshd.util.test.CommonTestSupportUtils;
import org.apache.sshd.util.test.ContainerTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.images.builder.dockerfile.DockerfileBuilder;
import org.testcontainers.utility.MountableFile;

/**
* Tests to ensure that an Apache MINA sshd client can talk to OpenSSH servers with or without "strict KEX". This
* implicitly tests the message sequence number handling; if sequence numbers get out of sync or are reset wrongly,
* subsequent messages cannot be decrypted correctly and there will be exceptions.
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
* @see <A HREF="https://github.com/apache/mina-sshd/issues/445">Terrapin Mitigation: &quot;strict-kex&quot;</A>
*/
@Category(ContainerTestCase.class)
public class StrictKexInteroperabilityTest extends BaseTestSupport {

private static final Logger LOG = LoggerFactory.getLogger(StrictKexInteroperabilityTest.class);

private static final String TEST_RESOURCES = "org/apache/sshd/common/kex/extensions/client";

private SshClient client;

public StrictKexInteroperabilityTest() {
super();
}

@Before
public void setUp() throws Exception {
client = setupTestClient();
SessionFactory factory = new TestSessionFactory(client);
client.setSessionFactory(factory);
}

@After
public void tearDown() throws Exception {
if (client != null) {
client.stop();
}
}

private DockerfileBuilder strictKexImage(DockerfileBuilder builder, boolean withStrictKex) {
if (!withStrictKex) {
return builder
// CentOS 7 is EOL and thus unlikely to get the security update for strict KEX.
.from("centos:7.9.2009") //
.run("yum install -y openssh-server") // Installs OpenSSH 7.4
.run("/usr/sbin/sshd-keygen") // Generate multiple host keys
.run("adduser bob"); // Add a user
} else {
return builder
.from("alpine:20231219") //
.run("apk --update add openssh-server") // Installs OpenSSH 9.6
.run("ssh-keygen -A") // Generate multiple host keys
.run("adduser -D bob") // Add a user
.run("echo 'bob:passwordBob' | chpasswd"); // Give it a password to unlock the user
}
}

@Test
public void testStrictKexOff() throws Exception {
testStrictKex(false);
}

@Test
public void testStrictKexOn() throws Exception {
testStrictKex(true);
}

private void testStrictKex(boolean withStrictKex) throws Exception {
// This tests that the message sequence numbers are handled correctly. Strict KEX resets them to zero on any
// KEX, without strict KEX, they're not reset. If sequence numbers get out of sync, received messages are
// decrypted wrongly and there will be exceptions.
@SuppressWarnings("resource")
GenericContainer<?> sshdContainer = new GenericContainer<>(new ImageFromDockerfile()
.withDockerfileFromBuilder(builder -> strictKexImage(builder, withStrictKex) //
.run("mkdir -p /home/bob/.ssh") // Create the SSH config directory
.entryPoint("/entrypoint.sh") //
.build())) //
.withCopyFileToContainer(MountableFile.forClasspathResource(TEST_RESOURCES + "/bob_key.pub"),
"/home/bob/.ssh/authorized_keys")
// entrypoint must be executable. Spotbugs doesn't like 0777, so use hex
.withCopyFileToContainer(
MountableFile.forClasspathResource(TEST_RESOURCES + "/entrypoint.sh", 0x1ff),
"/entrypoint.sh")
.waitingFor(Wait.forLogMessage(".*Server listening on :: port 22.*\\n", 1)) //
.withExposedPorts(22) //
.withLogConsumer(new Slf4jLogConsumer(LOG));
sshdContainer.start();
try {
FileKeyPairProvider keyPairProvider = CommonTestSupportUtils.createTestKeyPairProvider(TEST_RESOURCES + "/bob_key");
client.setKeyIdentityProvider(keyPairProvider);
client.start();
try (ClientSession session = client.connect("bob", sshdContainer.getHost(), sshdContainer.getMappedPort(22))
.verify(CONNECT_TIMEOUT).getSession()) {
session.auth().verify(AUTH_TIMEOUT);
assertTrue("Should authenticate", session.isAuthenticated());
assertTrue("Unexpected session type " + session.getClass().getName(), session instanceof TestSession);
assertEquals("Unexpected strict KEX usage", withStrictKex, ((TestSession) session).usesStrictKex());
try (ChannelShell channel = session.createShellChannel()) {
channel.setOut(System.out);
channel.setErr(System.err);
channel.setStreaming(StreamingChannel.Streaming.Sync);
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
channel.setIn(pis);
assertTrue("Could not open session", channel.open().await(DEFAULT_TIMEOUT));
LOG.info("writing some data...");
pos.write("\n\n".getBytes(StandardCharsets.UTF_8));
assertTrue("Channel should be open", channel.isOpen());
assertTrue(session.reExchangeKeys().verify(CONNECT_TIMEOUT).isDone());
assertTrue("Channel should be open", channel.isOpen());
LOG.info("writing some data...");
pos.write("\n\n".getBytes(StandardCharsets.UTF_8));
assertTrue("Channel should be open", channel.isOpen());
channel.close(true);
}
}
} finally {
sshdContainer.stop();
}
}

// Subclass ClientSessionImpl to get access to the strictKex flag.

private static class TestSessionFactory extends SessionFactory {

TestSessionFactory(ClientFactoryManager client) {
super(client);
}

@Override
protected ClientSessionImpl doCreateSession(IoSession ioSession) throws Exception {
return new TestSession(getClient(), ioSession);
}
}

private static class TestSession extends ClientSessionImpl {

TestSession(ClientFactoryManager client, IoSession ioSession) throws Exception {
super(client, ioSession);
}

boolean usesStrictKex() {
return strictKex;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAxY3Hr1SqpJIQ9SbFfGMGweVy8jg2TEH3GC1K0LudQHJwogRi
+debdCqUtuSITbpPhjkeZSk9rq198d6RhT6TQmY9J8wLL2/+VXZk/rMVEEjeXQS3
ImRnL2vVmkAunv6LwfDGHIovkhwj3/lqGWphDAKnHyXusPDwQ3N4LFGgxwXvRGqc
lzmP8H+KDWaaPapk1AZCBIoD4JbL8faBtLNU01r+pB3sIKvfsPJ5DxPErThfrPuD
qIbA3axEqFlgX4aVl3yMnSWjfhLhO7xD3YwrtUhannHt8pZQo5FkwCGWDpkG3xs+
qK3ZACrhMFMTvPuDS83jDtEzNd5KYb4KnkOPMQIDAQABAoIBAQCE5GktgrD/39pU
b25tzFehW25FjpbIGZ/UvbMUUwDnd5RZCMZj9yv1qyc7GOSwFOKmEgpmVqXNuZt9
dxFBJuT8x7Xf7Zygnp/icbBivakvuTUMMb3X/t6CwfGAwCgcgHMXVZaPYE275f4k
Dq3Wxv7di3NMusGkeY/GcAipF4gmGKKe7Ck1ifRypF2cDJsgTtsoFUHNNKfnT3gf
OcJsVLRl0osbsxdqU+Tep46+jHrNt8J9n2VeRNRIqGHj0CkNdpLQOs+MjvIO3Hgq
9NUxwIExwaPnBpTLlWwfemCz3JQnlAineMbYBGa1tpAA3Iw56NWcNbiOPyUyffbI
wBC4r1uZAoGBAPESsergFD+ontChEI+h38oM/D9DKCObZR2kz6WArZ54i1dJWOgh
HCsuxgPjxmaddPKghfNhUORdZBynuS5G7n6BfItNilDiFm2KBk12d38OVovUFo1Q
r5akclKf0kFxHt5TzHIrNAv7B4OF0Uk3kuDHM7ITX3qDpTSBLlzPAUUHAoGBANHJ
QIPmuF2q+PXnnSgdEyiETfl/IqUTXQyxda8kRIPJKKHZKPHZePhgJKUq9VP32PrP
AxIBNrS3Netsp+EAApj09hmWUcgJRIU1/wjpVGqUmguYgh8nVFOPDudOJD5ltQ/A
enzQ19IkGroaQB8CBGZsPaBAvqRZ5PLbm+BZEPQHAoGAblaMMGCXY/udlQfjOJpy
f1wqKBpoyMNbKJJCqBGZZaruu+jKVJSy++DQqP8b0+PFnzdxl8+24o8MP0FVNKUq
i6RgiLHY2ORiN4ixEctjLjg1zJIqMEv50g06di7IYUORSVk5fhfgHourCLu66rQQ
+eiy9JKBZOXUO4/U1I26mwkCgYAhfuCuLsiBLCtUGAcfwISuk3FfxMzjTpQs0qjX
rhLCd/vk26eN9gs6nR88v/8ryQb8BNGYrljtwdL6I/8qDbZcdcBVlYq5RcGLA3QV
GCxCWDfAYjlkgAMW1GCsze07iUG/ohvskevjwaAC1u4mBUxujhnI3I2T8EZ+AFKD
H7V1QQKBgQDNt+zjSdLtA9AczxDwWmi5SbS+k+nGbi6AQO9i73wky/wxx7FonfWS
2skkOUIst3HBc0Oz+CJTfNFQK6GVqtzTdlZFhMYS0ua1Djd6q6S648+K0cieY4r5
5irivHYVN8t7lBcvbA7E7yD6dHXSHsn6yOLTrV382qRfJTbxG7ZVWA==
-----END RSA PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFjcevVKqkkhD1JsV8YwbB5XLyODZMQfcYLUrQu51AcnCiBGL515t0KpS25IhNuk+GOR5lKT2urX3x3pGFPpNCZj0nzAsvb/5VdmT+sxUQSN5dBLciZGcva9WaQC6e/ovB8MYcii+SHCPf+WoZamEMAqcfJe6w8PBDc3gsUaDHBe9EapyXOY/wf4oNZpo9qmTUBkIEigPglsvx9oG0s1TTWv6kHewgq9+w8nkPE8StOF+s+4OohsDdrESoWWBfhpWXfIydJaN+EuE7vEPdjCu1SFqece3yllCjkWTAIZYOmQbfGz6ordkAKuEwUxO8+4NLzeMO0TM13kphvgqeQ48x user01
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

chown -R bob /home/bob
chmod 0600 /home/bob/.ssh/*

/usr/sbin/sshd -D -ddd
1 change: 1 addition & 0 deletions sshd-mina/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
<exclude>**/SessionReKeyHostKeyExchangeTest.java</exclude>
<exclude>**/HostBoundPubKeyAuthTest.java</exclude>
<exclude>**/PortForwardingWithOpenSshTest.java</exclude>
<exclude>**/StrictKexInteroperabilityTest.java</exclude>
<!-- reading files from classpath doesn't work correctly w/ reusable test jar -->
<exclude>**/OpenSSHCertificateTest.java</exclude>
</excludes>
Expand Down
1 change: 1 addition & 0 deletions sshd-netty/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
<exclude>**/SessionReKeyHostKeyExchangeTest.java</exclude>
<exclude>**/HostBoundPubKeyAuthTest.java</exclude>
<exclude>**/PortForwardingWithOpenSshTest.java</exclude>
<exclude>**/StrictKexInteroperabilityTest.java</exclude>
<!-- reading files from classpath doesn't work correctly w/ reusable test jar -->
<exclude>**/OpenSSHCertificateTest.java</exclude>
</excludes>
Expand Down

0 comments on commit 7b2c781

Please sign in to comment.