Skip to content

Commit 91ba60e

Browse files
author
Lyor Goldstein
committed
[apacheGH-445] Added StrictKexTest
1 parent 1c11e3a commit 91ba60e

File tree

2 files changed

+281
-1
lines changed

2 files changed

+281
-1
lines changed

sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ public static void outputDebugMessage(String format, Object o) {
703703

704704
public static void outputDebugMessage(String format, Object... args) {
705705
if (OUTPUT_DEBUG_MESSAGES) {
706-
outputDebugMessage(String.format(format, args));
706+
outputDebugMessage(GenericUtils.isEmpty(args) ? format : String.format(format, args));
707707
}
708708
}
709709

@@ -713,6 +713,24 @@ public static void outputDebugMessage(Object message) {
713713
}
714714
}
715715

716+
public static void failWithWrittenErrorMessage(String format, Object... args) {
717+
failWithWrittenErrorMessage(GenericUtils.isEmpty(args) ? format : String.format(format, args));
718+
}
719+
720+
public static void failWithWrittenErrorMessage(Object message) {
721+
writeErrorMessage(message);
722+
fail(Objects.toString(message));
723+
}
724+
725+
public static void writeErrorMessage(String format, Object... args) {
726+
writeErrorMessage(GenericUtils.isEmpty(args) ? format : String.format(format, args));
727+
}
728+
729+
public static void writeErrorMessage(Object message) {
730+
System.err.append("===[ERROR]=== ").println(message);
731+
System.err.flush();
732+
}
733+
716734
/* ---------------------------------------------------------------------------- */
717735

718736
public static void replaceJULLoggers() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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.IOException;
23+
import java.net.InetSocketAddress;
24+
import java.util.Map;
25+
26+
import org.apache.sshd.client.SshClient;
27+
import org.apache.sshd.client.session.ClientSession;
28+
import org.apache.sshd.common.future.KeyExchangeFuture;
29+
import org.apache.sshd.common.kex.KexProposalOption;
30+
import org.apache.sshd.common.session.Session;
31+
import org.apache.sshd.common.session.SessionListener;
32+
import org.apache.sshd.common.session.helpers.SessionCountersDetails;
33+
import org.apache.sshd.common.session.helpers.SessionKexDetails;
34+
import org.apache.sshd.common.util.net.SshdSocketAddress;
35+
import org.apache.sshd.core.CoreModuleProperties;
36+
import org.apache.sshd.server.SshServer;
37+
import org.apache.sshd.util.test.BaseTestSupport;
38+
import org.junit.After;
39+
import org.junit.Before;
40+
import org.junit.FixMethodOrder;
41+
import org.junit.Test;
42+
import org.junit.runners.MethodSorters;
43+
44+
/**
45+
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
46+
* @see <A HREF="https://github.com/apache/mina-sshd/issues/445">Terrapin Mitigation: &quot;strict-kex&quot;</A>
47+
*/
48+
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
49+
public class StrictKexTest extends BaseTestSupport {
50+
private SshServer sshd;
51+
private SshClient client;
52+
53+
public StrictKexTest() {
54+
super();
55+
}
56+
57+
@Override
58+
protected SshServer setupTestServer() {
59+
SshServer server = super.setupTestServer();
60+
CoreModuleProperties.USE_STRICT_KEX.set(server, true);
61+
return server;
62+
}
63+
64+
@Override
65+
protected SshClient setupTestClient() {
66+
SshClient sshc = super.setupTestClient();
67+
CoreModuleProperties.USE_STRICT_KEX.set(sshc, true);
68+
return sshc;
69+
}
70+
71+
@Before
72+
public void setUp() throws Exception {
73+
sshd = setupTestServer();
74+
client = setupTestClient();
75+
}
76+
77+
@After
78+
public void tearDown() throws Exception {
79+
if (sshd != null) {
80+
sshd.stop(true);
81+
}
82+
if (client != null) {
83+
client.stop();
84+
}
85+
}
86+
87+
@Test
88+
public void testConnectionClosedIfFirstPacketNotKexInit() throws Exception {
89+
// TODO
90+
}
91+
92+
@Test
93+
public void testStrictKexIgnoredIfNotFirstKexInit() throws Exception {
94+
// TODO
95+
}
96+
97+
@Test
98+
public void testRekeyResetsPacketSequenceNumbers() throws Exception {
99+
sshd.addSessionListener(new SessionListener() {
100+
private SessionKexDetails beforeDetails;
101+
private SessionCountersDetails beforeCounters;
102+
103+
@Override
104+
public void sessionNegotiationEnd(
105+
Session session, Map<KexProposalOption, String> clientProposal,
106+
Map<KexProposalOption, String> serverProposal, Map<KexProposalOption, String> negotiatedOptions,
107+
Throwable reason) {
108+
SessionKexDetails details = session.getSessionKexDetails();
109+
assertTrue("StrictKexSignalled[server]", details.isStrictKexSignalled());
110+
111+
SessionCountersDetails counters = session.getSessionCountersDetails();
112+
if (beforeDetails == null) {
113+
beforeDetails = details;
114+
}
115+
if (beforeCounters == null) {
116+
beforeCounters = counters;
117+
}
118+
119+
// Use reference equality to detect if this is the 1st re-key
120+
if ((beforeDetails != details) || (beforeCounters != counters)) {
121+
// TODO assertSessionSequenceNumbersReset(session, beforeDetails, beforeCounters);
122+
}
123+
}
124+
});
125+
126+
try (ClientSession session = obtainInitialTestClientSession()) {
127+
SessionKexDetails beforeDetails = session.getSessionKexDetails();
128+
assertTrue("StrictKexSignalled[client]", beforeDetails.isStrictKexSignalled());
129+
130+
/*
131+
* Generate some traffic in order to pump-up the sequence numbers enough
132+
* so that the re-keying exchange will not reach this number after packets
133+
* sequence numbers reset. Usually ~10 extra messages should be enough
134+
*/
135+
136+
SessionCountersDetails beforeCounters = session.getSessionCountersDetails();
137+
KeyExchangeFuture rekeyFuture = session.reExchangeKeys();
138+
boolean exchanged = rekeyFuture.await(AUTH_TIMEOUT);
139+
assertTrue("Rekey exchange completed", exchanged);
140+
// TODO assertSessionSequenceNumbersReset(session, beforeDetails, beforeCounters);
141+
} finally {
142+
client.stop();
143+
}
144+
}
145+
146+
// NOTE: we use failWithWrittenErrorMessage in order to compensate for session timeout in case of debug breakpoint
147+
static void assertSessionSequenceNumbersReset(
148+
Session session, SessionKexDetails beforeDetails, SessionCountersDetails beforeCounters) {
149+
long incomingPacketSequenceNumberBefore = beforeCounters.getInputPacketSequenceNumber();
150+
long outputPacketSequenceNumberBefore = beforeCounters.getOutputPacketSequenceNumber();
151+
152+
SessionCountersDetails afterCounters = session.getSessionCountersDetails();
153+
long incomingPacketSequenceNumberAfter = afterCounters.getInputPacketSequenceNumber();
154+
long outputPacketSequenceNumberAfter = afterCounters.getOutputPacketSequenceNumber();
155+
156+
String sessionType = session.isServerSession() ? "server" : "client";
157+
if (incomingPacketSequenceNumberAfter > incomingPacketSequenceNumberBefore) {
158+
failWithWrittenErrorMessage(sessionType + ": Incoming packet sequence number not reset: before=" + incomingPacketSequenceNumberBefore + ", after=" + incomingPacketSequenceNumberAfter);
159+
}
160+
161+
if (outputPacketSequenceNumberAfter > outputPacketSequenceNumberBefore) {
162+
failWithWrittenErrorMessage(sessionType + ": Outgoing packet sequence number not reset: before=" + incomingPacketSequenceNumberBefore + ", after=" + incomingPacketSequenceNumberAfter);
163+
}
164+
165+
SessionKexDetails afterDetails = session.getSessionKexDetails();
166+
int beforeSentNewKeys = beforeDetails.getNewKeysSentCount();
167+
int afterSentNewKeys = afterDetails.getNewKeysSentCount();
168+
if (beforeSentNewKeys >= afterSentNewKeys) {
169+
failWithWrittenErrorMessage(sessionType + ": sent NEWKEY count not updated: before=" + beforeSentNewKeys + ", after=" + afterSentNewKeys);
170+
}
171+
172+
int beforeRcvdNewKeys = beforeDetails.getNewKeysReceivedCount();
173+
int afterRcvdNewKeys = afterDetails.getNewKeysReceivedCount();
174+
if (beforeRcvdNewKeys >= afterRcvdNewKeys) {
175+
failWithWrittenErrorMessage(sessionType + ": received NEWKEY count not updated: before=" + beforeRcvdNewKeys + ", after=" + afterRcvdNewKeys);
176+
}
177+
}
178+
179+
@Test
180+
public void testStrictKexNotActivatedIfClientDoesNotSupportIt() throws Exception {
181+
testStrictKexNotActivatedIfNotSupportByPeer(false);
182+
}
183+
184+
@Test
185+
public void testStrictKexNotActivatedIfServerDoesNotSupportIt() throws Exception {
186+
testStrictKexNotActivatedIfNotSupportByPeer(true);
187+
}
188+
189+
private void testStrictKexNotActivatedIfNotSupportByPeer(boolean clientSupported) throws Exception {
190+
if (clientSupported) {
191+
CoreModuleProperties.USE_STRICT_KEX.set(sshd, false);
192+
} else {
193+
CoreModuleProperties.USE_STRICT_KEX.set(client, false);
194+
}
195+
196+
sshd.addSessionListener(new SessionListener() {
197+
@Override
198+
public void sessionNegotiationEnd(
199+
Session session, Map<KexProposalOption, String> clientProposal,
200+
Map<KexProposalOption, String> serverProposal, Map<KexProposalOption, String> negotiatedOptions,
201+
Throwable reason) {
202+
SessionKexDetails details = session.getSessionKexDetails();
203+
assertEquals("StrictKexEnabled[server]", !clientSupported, details.isStrictKexEnabled());
204+
assertFalse("StrictKexSignalled[server]", details.isStrictKexSignalled());
205+
}
206+
});
207+
208+
try (ClientSession session = obtainInitialTestClientSession()) {
209+
SessionKexDetails details = session.getSessionKexDetails();
210+
assertEquals("StrictKexEnabled[client]", clientSupported, details.isStrictKexEnabled());
211+
assertFalse("StrictKexSignalled[client]", details.isStrictKexSignalled());
212+
} finally {
213+
client.stop();
214+
}
215+
}
216+
217+
private ClientSession obtainInitialTestClientSession() throws IOException {
218+
sshd.start();
219+
int port = sshd.getPort();
220+
221+
client.start();
222+
return createTestClientSession(port);
223+
}
224+
225+
private ClientSession createTestClientSession(int port) throws IOException {
226+
ClientSession session = createTestClientSession(TEST_LOCALHOST, port);
227+
try {
228+
InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
229+
assertEquals("Mismatched connect host", TEST_LOCALHOST, addr.getHostString());
230+
231+
ClientSession returnValue = session;
232+
session = null; // avoid 'finally' close
233+
return returnValue;
234+
} finally {
235+
if (session != null) {
236+
session.close();
237+
}
238+
}
239+
}
240+
241+
private ClientSession createTestClientSession(String host, int port) throws IOException {
242+
ClientSession session = client.connect(getCurrentTestName(), host, port)
243+
.verify(CONNECT_TIMEOUT).getSession();
244+
try {
245+
session.addPasswordIdentity(getCurrentTestName());
246+
session.auth().verify(AUTH_TIMEOUT);
247+
248+
InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
249+
assertNotNull("No reported connect address", addr);
250+
assertEquals("Mismatched connect port", port, addr.getPort());
251+
252+
ClientSession returnValue = session;
253+
session = null; // avoid 'finally' close
254+
return returnValue;
255+
} finally {
256+
if (session != null) {
257+
session.close();
258+
}
259+
}
260+
}
261+
262+
}

0 commit comments

Comments
 (0)