Skip to content

Commit

Permalink
[SSHD-1264] Add a test case
Browse files Browse the repository at this point in the history
Use a CentOS container running OpenSSH 7.4 configured to re-key every
second. Connect with an Apache MINA sshd client and write some data
every second for 20 seconds. Check that we got multiple key exchanges.

Install a ServerKeyVerifier that fails if the host key on re-keying is
different from the key presented in the initial KEX.

Note that the problem is not reproducible with newer OpenSSH (as
available for instance for Alpine containers).
  • Loading branch information
jtnord authored and tomaswolf committed Apr 27, 2022
1 parent 0e2a806 commit aafb49d
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* 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:https://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.client.session;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.util.concurrent.TimeUnit;

import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ChannelShell;
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
import org.apache.sshd.common.channel.StreamingChannel;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.session.SessionHeartbeatController;
import org.apache.sshd.util.test.BaseTestSupport;
import org.apache.sshd.util.test.ContainerTestCase;
import org.hamcrest.Description;
import org.hamcrest.MatcherAssert;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.junit.Rule;
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;

@Category(ContainerTestCase.class)
public class SessionReKeyHostKeyExchangeTest extends BaseTestSupport {

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

@Rule
public GenericContainer<?> sshdContainer = new GenericContainer<>(
new ImageFromDockerfile()
.withDockerfileFromBuilder(builder -> builder
// With alpine, which installs newer OpenSSH versions, SSHD-1264 does not occur.
.from("centos:7.9.2009") // So use CentOS, even if it's much slower
.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
.run("echo \"123qweASD\" | passwd bob --stdin") // Give the user a password
.run("echo RekeyLimit default 1 >> /etc/ssh/sshd_config") // Re-key every second
.entryPoint("/usr/sbin/sshd", "-D", "-ddd") //
.build()))
.waitingFor(Wait.forLogMessage(".*Server listening on :: port 22.*\\n", 1))
.withExposedPorts(22) //
.withLogConsumer(new Slf4jLogConsumer(LOG));

public SessionReKeyHostKeyExchangeTest() {
super();
}

@Test // https://issues.apache.org/jira/browse/SSHD-1264
public void testRekeyUsesSameHostKeyAlgorithm() throws Exception {
LOG.info("*************************************************************************************");
SshClient client = SshClient.setUpDefaultClient();
AcceptFirstAlgorithmHostKeyVerifier hostKeyVerifier = new AcceptFirstAlgorithmHostKeyVerifier();
client.setServerKeyVerifier(hostKeyVerifier);

try {
client.start();
try (ClientSession session = client.connect("bob", sshdContainer.getHost(), sshdContainer.getMappedPort(22))
.verify(CONNECT_TIMEOUT).getSession()) {
session.addPasswordIdentity("123qweASD");
assertTrue("Could not authenticate", session.auth().await(AUTH_TIMEOUT));
session.setSessionHeartbeat(SessionHeartbeatController.HeartbeatType.IGNORE, TimeUnit.MILLISECONDS, 1);

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));
for (int i = 0; i < 20; i++) {
Thread.sleep(1000);
LOG.info("writing some data...");
pos.write("\n\n".getBytes(StandardCharsets.UTF_8));
}
channel.close(true);
} catch (IOException e) {
// When KEX fails, we most likely get an exception on the PipedInputStream.
// Let's produce a halfway reasonable test failure.
assertEquals("Expected no host key changes in KEX", 0, hostKeyVerifier.errors);
throw new AssertionError("Exception in test", e);
}
Thread.sleep(5_000);
assertTrue("Session should still be open", session.isOpen());
}
// We should have about 25 key exchanges, but anything greater than 10 is fine.
MatcherAssert.assertThat("Not enough re-key attempts", hostKeyVerifier.verifications,
AtLeastMatcher.greaterThan(10));
} finally {
client.stop();
}

}

private static class AcceptFirstAlgorithmHostKeyVerifier implements ServerKeyVerifier {

volatile int errors;

volatile int verifications;

private PublicKey hostKey;

AcceptFirstAlgorithmHostKeyVerifier() {
super();
}

@Override
public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
if (hostKey == null) {
// first connect to this host we allow it and store the host key
hostKey = serverKey;
LOG.info("**** Accepting initial connection with host key algorithm {}", serverKey.getAlgorithm());
verifications++;
return true;
}

boolean sameKey = KeyUtils.compareKeys(hostKey, serverKey);
if (sameKey) {
LOG.info("Accepting subsequent hostkey, same as in initial connection");
} else {
LOG.error("**** Rejecting subsequent host key of type {}, inital host key was {}", serverKey.getAlgorithm(),
hostKey.getAlgorithm());
errors++;
}
verifications++;
return sameKey;
}
}

private static final class AtLeastMatcher extends TypeSafeDiagnosingMatcher<Integer> {

private final int atLeast;

private AtLeastMatcher(int atLeast) {
this.atLeast = atLeast;
}

@Override
protected boolean matchesSafely(Integer item, Description mismatchDescription) {
mismatchDescription.appendValue(item);
return atLeast <= item.intValue();
}

@Override
public void describeTo(Description description) {
description.appendText("a number greater than ").appendValue(atLeast);
}

public static AtLeastMatcher greaterThan(int atLeast) {
return new AtLeastMatcher(atLeast);
}
}
}
1 change: 1 addition & 0 deletions sshd-mina/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
<exclude>**/AsyncAuthInteractiveTest.java</exclude>
<!-- testcontainers filesystem building from classpath doesn't work from reusable test jar classpath -->
<exclude>**/ClientOpenSSHCertificatesTest.java</exclude>
<exclude>**/SessionReKeyHostKeyExchangeTest.java</exclude>
<!-- reading files from classpath doesn't work correctly w/ reusable test jar -->
<exclude>**/OpenSSHCertificateParserTest.java</exclude>
<exclude>**/GenerateOpenSSHClientCertificateTest.java</exclude>
Expand Down
1 change: 1 addition & 0 deletions sshd-netty/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
<exclude>**/Sshd1033Test.java</exclude>
<!-- testcontainers filesystem building from classpath doesn't work from reusable test jar classpath -->
<exclude>**/ClientOpenSSHCertificatesTest.java</exclude>
<exclude>**/SessionReKeyHostKeyExchangeTest.java</exclude>
<!-- reading files from classpath doesn't work correctly w/ reusable test jar -->
<exclude>**/OpenSSHCertificateParserTest.java</exclude>
<exclude>**/GenerateOpenSSHClientCertificateTest.java</exclude>
Expand Down

0 comments on commit aafb49d

Please sign in to comment.