Skip to content

Commit 7b2c781

Browse files
committed
apacheGH-445: strict KEX interoperability tests
Run an Apache MINA sshd client against OpenSSH servers that do have or do not have strict KEX.
1 parent 315739e commit 7b2c781

File tree

6 files changed

+228
-0
lines changed

6 files changed

+228
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.sshd.common.kex.extension;
21+
22+
import java.io.PipedInputStream;
23+
import java.io.PipedOutputStream;
24+
import java.nio.charset.StandardCharsets;
25+
26+
import org.apache.sshd.client.ClientFactoryManager;
27+
import org.apache.sshd.client.SshClient;
28+
import org.apache.sshd.client.channel.ChannelShell;
29+
import org.apache.sshd.client.session.ClientSession;
30+
import org.apache.sshd.client.session.ClientSessionImpl;
31+
import org.apache.sshd.client.session.SessionFactory;
32+
import org.apache.sshd.common.channel.StreamingChannel;
33+
import org.apache.sshd.common.io.IoSession;
34+
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
35+
import org.apache.sshd.util.test.BaseTestSupport;
36+
import org.apache.sshd.util.test.CommonTestSupportUtils;
37+
import org.apache.sshd.util.test.ContainerTestCase;
38+
import org.junit.After;
39+
import org.junit.Before;
40+
import org.junit.Test;
41+
import org.junit.experimental.categories.Category;
42+
import org.slf4j.Logger;
43+
import org.slf4j.LoggerFactory;
44+
import org.testcontainers.containers.GenericContainer;
45+
import org.testcontainers.containers.output.Slf4jLogConsumer;
46+
import org.testcontainers.containers.wait.strategy.Wait;
47+
import org.testcontainers.images.builder.ImageFromDockerfile;
48+
import org.testcontainers.images.builder.dockerfile.DockerfileBuilder;
49+
import org.testcontainers.utility.MountableFile;
50+
51+
/**
52+
* Tests to ensure that an Apache MINA sshd client can talk to OpenSSH servers with or without "strict KEX". This
53+
* implicitly tests the message sequence number handling; if sequence numbers get out of sync or are reset wrongly,
54+
* subsequent messages cannot be decrypted correctly and there will be exceptions.
55+
*
56+
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
57+
* @see <A HREF="https://github.com/apache/mina-sshd/issues/445">Terrapin Mitigation: &quot;strict-kex&quot;</A>
58+
*/
59+
@Category(ContainerTestCase.class)
60+
public class StrictKexInteroperabilityTest extends BaseTestSupport {
61+
62+
private static final Logger LOG = LoggerFactory.getLogger(StrictKexInteroperabilityTest.class);
63+
64+
private static final String TEST_RESOURCES = "org/apache/sshd/common/kex/extensions/client";
65+
66+
private SshClient client;
67+
68+
public StrictKexInteroperabilityTest() {
69+
super();
70+
}
71+
72+
@Before
73+
public void setUp() throws Exception {
74+
client = setupTestClient();
75+
SessionFactory factory = new TestSessionFactory(client);
76+
client.setSessionFactory(factory);
77+
}
78+
79+
@After
80+
public void tearDown() throws Exception {
81+
if (client != null) {
82+
client.stop();
83+
}
84+
}
85+
86+
private DockerfileBuilder strictKexImage(DockerfileBuilder builder, boolean withStrictKex) {
87+
if (!withStrictKex) {
88+
return builder
89+
// CentOS 7 is EOL and thus unlikely to get the security update for strict KEX.
90+
.from("centos:7.9.2009") //
91+
.run("yum install -y openssh-server") // Installs OpenSSH 7.4
92+
.run("/usr/sbin/sshd-keygen") // Generate multiple host keys
93+
.run("adduser bob"); // Add a user
94+
} else {
95+
return builder
96+
.from("alpine:20231219") //
97+
.run("apk --update add openssh-server") // Installs OpenSSH 9.6
98+
.run("ssh-keygen -A") // Generate multiple host keys
99+
.run("adduser -D bob") // Add a user
100+
.run("echo 'bob:passwordBob' | chpasswd"); // Give it a password to unlock the user
101+
}
102+
}
103+
104+
@Test
105+
public void testStrictKexOff() throws Exception {
106+
testStrictKex(false);
107+
}
108+
109+
@Test
110+
public void testStrictKexOn() throws Exception {
111+
testStrictKex(true);
112+
}
113+
114+
private void testStrictKex(boolean withStrictKex) throws Exception {
115+
// This tests that the message sequence numbers are handled correctly. Strict KEX resets them to zero on any
116+
// KEX, without strict KEX, they're not reset. If sequence numbers get out of sync, received messages are
117+
// decrypted wrongly and there will be exceptions.
118+
@SuppressWarnings("resource")
119+
GenericContainer<?> sshdContainer = new GenericContainer<>(new ImageFromDockerfile()
120+
.withDockerfileFromBuilder(builder -> strictKexImage(builder, withStrictKex) //
121+
.run("mkdir -p /home/bob/.ssh") // Create the SSH config directory
122+
.entryPoint("/entrypoint.sh") //
123+
.build())) //
124+
.withCopyFileToContainer(MountableFile.forClasspathResource(TEST_RESOURCES + "/bob_key.pub"),
125+
"/home/bob/.ssh/authorized_keys")
126+
// entrypoint must be executable. Spotbugs doesn't like 0777, so use hex
127+
.withCopyFileToContainer(
128+
MountableFile.forClasspathResource(TEST_RESOURCES + "/entrypoint.sh", 0x1ff),
129+
"/entrypoint.sh")
130+
.waitingFor(Wait.forLogMessage(".*Server listening on :: port 22.*\\n", 1)) //
131+
.withExposedPorts(22) //
132+
.withLogConsumer(new Slf4jLogConsumer(LOG));
133+
sshdContainer.start();
134+
try {
135+
FileKeyPairProvider keyPairProvider = CommonTestSupportUtils.createTestKeyPairProvider(TEST_RESOURCES + "/bob_key");
136+
client.setKeyIdentityProvider(keyPairProvider);
137+
client.start();
138+
try (ClientSession session = client.connect("bob", sshdContainer.getHost(), sshdContainer.getMappedPort(22))
139+
.verify(CONNECT_TIMEOUT).getSession()) {
140+
session.auth().verify(AUTH_TIMEOUT);
141+
assertTrue("Should authenticate", session.isAuthenticated());
142+
assertTrue("Unexpected session type " + session.getClass().getName(), session instanceof TestSession);
143+
assertEquals("Unexpected strict KEX usage", withStrictKex, ((TestSession) session).usesStrictKex());
144+
try (ChannelShell channel = session.createShellChannel()) {
145+
channel.setOut(System.out);
146+
channel.setErr(System.err);
147+
channel.setStreaming(StreamingChannel.Streaming.Sync);
148+
PipedOutputStream pos = new PipedOutputStream();
149+
PipedInputStream pis = new PipedInputStream(pos);
150+
channel.setIn(pis);
151+
assertTrue("Could not open session", channel.open().await(DEFAULT_TIMEOUT));
152+
LOG.info("writing some data...");
153+
pos.write("\n\n".getBytes(StandardCharsets.UTF_8));
154+
assertTrue("Channel should be open", channel.isOpen());
155+
assertTrue(session.reExchangeKeys().verify(CONNECT_TIMEOUT).isDone());
156+
assertTrue("Channel should be open", channel.isOpen());
157+
LOG.info("writing some data...");
158+
pos.write("\n\n".getBytes(StandardCharsets.UTF_8));
159+
assertTrue("Channel should be open", channel.isOpen());
160+
channel.close(true);
161+
}
162+
}
163+
} finally {
164+
sshdContainer.stop();
165+
}
166+
}
167+
168+
// Subclass ClientSessionImpl to get access to the strictKex flag.
169+
170+
private static class TestSessionFactory extends SessionFactory {
171+
172+
TestSessionFactory(ClientFactoryManager client) {
173+
super(client);
174+
}
175+
176+
@Override
177+
protected ClientSessionImpl doCreateSession(IoSession ioSession) throws Exception {
178+
return new TestSession(getClient(), ioSession);
179+
}
180+
}
181+
182+
private static class TestSession extends ClientSessionImpl {
183+
184+
TestSession(ClientFactoryManager client, IoSession ioSession) throws Exception {
185+
super(client, ioSession);
186+
}
187+
188+
boolean usesStrictKex() {
189+
return strictKex;
190+
}
191+
}
192+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEpAIBAAKCAQEAxY3Hr1SqpJIQ9SbFfGMGweVy8jg2TEH3GC1K0LudQHJwogRi
3+
+debdCqUtuSITbpPhjkeZSk9rq198d6RhT6TQmY9J8wLL2/+VXZk/rMVEEjeXQS3
4+
ImRnL2vVmkAunv6LwfDGHIovkhwj3/lqGWphDAKnHyXusPDwQ3N4LFGgxwXvRGqc
5+
lzmP8H+KDWaaPapk1AZCBIoD4JbL8faBtLNU01r+pB3sIKvfsPJ5DxPErThfrPuD
6+
qIbA3axEqFlgX4aVl3yMnSWjfhLhO7xD3YwrtUhannHt8pZQo5FkwCGWDpkG3xs+
7+
qK3ZACrhMFMTvPuDS83jDtEzNd5KYb4KnkOPMQIDAQABAoIBAQCE5GktgrD/39pU
8+
b25tzFehW25FjpbIGZ/UvbMUUwDnd5RZCMZj9yv1qyc7GOSwFOKmEgpmVqXNuZt9
9+
dxFBJuT8x7Xf7Zygnp/icbBivakvuTUMMb3X/t6CwfGAwCgcgHMXVZaPYE275f4k
10+
Dq3Wxv7di3NMusGkeY/GcAipF4gmGKKe7Ck1ifRypF2cDJsgTtsoFUHNNKfnT3gf
11+
OcJsVLRl0osbsxdqU+Tep46+jHrNt8J9n2VeRNRIqGHj0CkNdpLQOs+MjvIO3Hgq
12+
9NUxwIExwaPnBpTLlWwfemCz3JQnlAineMbYBGa1tpAA3Iw56NWcNbiOPyUyffbI
13+
wBC4r1uZAoGBAPESsergFD+ontChEI+h38oM/D9DKCObZR2kz6WArZ54i1dJWOgh
14+
HCsuxgPjxmaddPKghfNhUORdZBynuS5G7n6BfItNilDiFm2KBk12d38OVovUFo1Q
15+
r5akclKf0kFxHt5TzHIrNAv7B4OF0Uk3kuDHM7ITX3qDpTSBLlzPAUUHAoGBANHJ
16+
QIPmuF2q+PXnnSgdEyiETfl/IqUTXQyxda8kRIPJKKHZKPHZePhgJKUq9VP32PrP
17+
AxIBNrS3Netsp+EAApj09hmWUcgJRIU1/wjpVGqUmguYgh8nVFOPDudOJD5ltQ/A
18+
enzQ19IkGroaQB8CBGZsPaBAvqRZ5PLbm+BZEPQHAoGAblaMMGCXY/udlQfjOJpy
19+
f1wqKBpoyMNbKJJCqBGZZaruu+jKVJSy++DQqP8b0+PFnzdxl8+24o8MP0FVNKUq
20+
i6RgiLHY2ORiN4ixEctjLjg1zJIqMEv50g06di7IYUORSVk5fhfgHourCLu66rQQ
21+
+eiy9JKBZOXUO4/U1I26mwkCgYAhfuCuLsiBLCtUGAcfwISuk3FfxMzjTpQs0qjX
22+
rhLCd/vk26eN9gs6nR88v/8ryQb8BNGYrljtwdL6I/8qDbZcdcBVlYq5RcGLA3QV
23+
GCxCWDfAYjlkgAMW1GCsze07iUG/ohvskevjwaAC1u4mBUxujhnI3I2T8EZ+AFKD
24+
H7V1QQKBgQDNt+zjSdLtA9AczxDwWmi5SbS+k+nGbi6AQO9i73wky/wxx7FonfWS
25+
2skkOUIst3HBc0Oz+CJTfNFQK6GVqtzTdlZFhMYS0ua1Djd6q6S648+K0cieY4r5
26+
5irivHYVN8t7lBcvbA7E7yD6dHXSHsn6yOLTrV382qRfJTbxG7ZVWA==
27+
-----END RSA PRIVATE KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFjcevVKqkkhD1JsV8YwbB5XLyODZMQfcYLUrQu51AcnCiBGL515t0KpS25IhNuk+GOR5lKT2urX3x3pGFPpNCZj0nzAsvb/5VdmT+sxUQSN5dBLciZGcva9WaQC6e/ovB8MYcii+SHCPf+WoZamEMAqcfJe6w8PBDc3gsUaDHBe9EapyXOY/wf4oNZpo9qmTUBkIEigPglsvx9oG0s1TTWv6kHewgq9+w8nkPE8StOF+s+4OohsDdrESoWWBfhpWXfIydJaN+EuE7vEPdjCu1SFqece3yllCjkWTAIZYOmQbfGz6ordkAKuEwUxO8+4NLzeMO0TM13kphvgqeQ48x user01
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
3+
chown -R bob /home/bob
4+
chmod 0600 /home/bob/.ssh/*
5+
6+
/usr/sbin/sshd -D -ddd

sshd-mina/pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
<exclude>**/SessionReKeyHostKeyExchangeTest.java</exclude>
125125
<exclude>**/HostBoundPubKeyAuthTest.java</exclude>
126126
<exclude>**/PortForwardingWithOpenSshTest.java</exclude>
127+
<exclude>**/StrictKexInteroperabilityTest.java</exclude>
127128
<!-- reading files from classpath doesn't work correctly w/ reusable test jar -->
128129
<exclude>**/OpenSSHCertificateTest.java</exclude>
129130
</excludes>

sshd-netty/pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
<exclude>**/SessionReKeyHostKeyExchangeTest.java</exclude>
144144
<exclude>**/HostBoundPubKeyAuthTest.java</exclude>
145145
<exclude>**/PortForwardingWithOpenSshTest.java</exclude>
146+
<exclude>**/StrictKexInteroperabilityTest.java</exclude>
146147
<!-- reading files from classpath doesn't work correctly w/ reusable test jar -->
147148
<exclude>**/OpenSSHCertificateTest.java</exclude>
148149
</excludes>

0 commit comments

Comments
 (0)