Skip to content

Commit 315739e

Browse files
committed
apacheGH-445: Unit tests for strict KEX
Add tests for the restricted message handling if strict KEX is active: * Initial KEX fails if KEX_INIT is not the first message * Initial KEX fails if there are spurious messages like DEBUG during KEX * Re-KEX succeeds even if there are spurious messages
1 parent 6b0fd46 commit 315739e

File tree

1 file changed

+264
-0
lines changed

1 file changed

+264
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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.util.Map;
24+
import java.util.concurrent.atomic.AtomicBoolean;
25+
import java.util.concurrent.atomic.AtomicReference;
26+
27+
import org.apache.sshd.client.SshClient;
28+
import org.apache.sshd.client.session.ClientSession;
29+
import org.apache.sshd.common.SshConstants;
30+
import org.apache.sshd.common.SshException;
31+
import org.apache.sshd.common.io.IoWriteFuture;
32+
import org.apache.sshd.common.kex.KexProposalOption;
33+
import org.apache.sshd.common.session.Session;
34+
import org.apache.sshd.common.session.SessionListener;
35+
import org.apache.sshd.common.util.GenericUtils;
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+
* Tests for message handling during "strict KEX" is active: initial KEX must fail and disconnect if the KEX_INIT
46+
* message is not first, or if there are spurious extra messages like IGNORE or DEBUG during KEX. Later KEXes must
47+
* succeed even if there are spurious messages.
48+
* <p>
49+
* The other part of "strict KEX" is resetting the message sequence numbers after KEX. This is not tested here but in
50+
* the {@link StrictKexInteroperabilityTest}, which runs an Apache MINA sshd client against OpenSSH servers that have or
51+
* do not have the "strict KEX" extension. If the sequence number handling was wrong, those tests would fail.
52+
* </p>
53+
*
54+
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
55+
* @see <A HREF="https://github.com/apache/mina-sshd/issues/445">Terrapin Mitigation: &quot;strict-kex&quot;</A>
56+
*/
57+
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
58+
public class StrictKexTest extends BaseTestSupport {
59+
private SshServer sshd;
60+
private SshClient client;
61+
62+
public StrictKexTest() {
63+
super();
64+
}
65+
66+
@Before
67+
public void setUp() throws Exception {
68+
sshd = setupTestServer();
69+
client = setupTestClient();
70+
}
71+
72+
@After
73+
public void tearDown() throws Exception {
74+
if (sshd != null) {
75+
sshd.stop(true);
76+
}
77+
if (client != null) {
78+
client.stop();
79+
}
80+
}
81+
82+
@Test
83+
public void connectionClosedIfFirstPacketFromClientNotKexInit() throws Exception {
84+
testConnectionClosedIfFirstPacketFromPeerNotKexInit(true);
85+
}
86+
87+
@Test
88+
public void connectionClosedIfFirstPacketFromServerNotKexInit() throws Exception {
89+
testConnectionClosedIfFirstPacketFromPeerNotKexInit(false);
90+
}
91+
92+
private void testConnectionClosedIfFirstPacketFromPeerNotKexInit(boolean clientInitiates) throws Exception {
93+
AtomicReference<IoWriteFuture> debugMsg = new AtomicReference<>();
94+
SessionListener messageInitiator = new SessionListener() {
95+
@Override // At this stage KEX-INIT not sent yet
96+
public void sessionNegotiationOptionsCreated(Session session, Map<KexProposalOption, String> proposal) {
97+
try {
98+
debugMsg.set(session.sendDebugMessage(true, getCurrentTestName(), null));
99+
} catch (Exception e) {
100+
throw new RuntimeException(e);
101+
}
102+
}
103+
};
104+
105+
if (clientInitiates) {
106+
client.addSessionListener(messageInitiator);
107+
} else {
108+
sshd.addSessionListener(messageInitiator);
109+
}
110+
111+
try (ClientSession session = obtainInitialTestClientSession()) {
112+
fail("Unexpected session success");
113+
} catch (SshException e) {
114+
IoWriteFuture future = debugMsg.get();
115+
assertNotNull("No SSH_MSG_DEBUG", future);
116+
assertTrue("SSH_MSG_DEBUG should have been sent", future.isWritten());
117+
// Due to a race condition in the Nio2 transport when closing a connection due to an exception it's possible
118+
// that we do _not_ get the expected disconnection code. The race condition may lead to the IoSession being
119+
// closed in the peer before it has sent the DISCONNECT message. Happens in particular on Windows.
120+
if (e.getDisconnectCode() == SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED) {
121+
assertTrue("Unexpected disconnect reason: " + e.getMessage(), e.getMessage()
122+
.startsWith("Strict KEX negotiated but sequence number of first KEX_INIT received is not 1"));
123+
}
124+
}
125+
}
126+
127+
@Test
128+
public void connectionClosedIfSpuriousPacketFromClientInKex() throws Exception {
129+
testConnectionClosedIfSupriousPacketInKex(true);
130+
}
131+
132+
@Test
133+
public void connectionClosedIfSpuriousPacketFromServerInKex() throws Exception {
134+
testConnectionClosedIfSupriousPacketInKex(false);
135+
}
136+
137+
private void testConnectionClosedIfSupriousPacketInKex(boolean clientInitiates) throws Exception {
138+
AtomicReference<IoWriteFuture> debugMsg = new AtomicReference<>();
139+
SessionListener messageInitiator = new SessionListener() {
140+
@Override // At this stage the peer's KEX_INIT has been received
141+
public void sessionNegotiationEnd(
142+
Session session, Map<KexProposalOption, String> clientProposal,
143+
Map<KexProposalOption, String> serverProposal, Map<KexProposalOption, String> negotiatedOptions,
144+
Throwable reason) {
145+
try {
146+
debugMsg.set(session.sendDebugMessage(true, getCurrentTestName(), null));
147+
} catch (Exception e) {
148+
throw new RuntimeException(e);
149+
}
150+
}
151+
};
152+
153+
if (clientInitiates) {
154+
client.addSessionListener(messageInitiator);
155+
} else {
156+
sshd.addSessionListener(messageInitiator);
157+
}
158+
159+
try (ClientSession session = obtainInitialTestClientSession()) {
160+
fail("Unexpected session success");
161+
} catch (SshException e) {
162+
IoWriteFuture future = debugMsg.get();
163+
assertNotNull("No SSH_MSG_DEBUG", future);
164+
assertTrue("SSH_MSG_DEBUG should have been sent", future.isWritten());
165+
if (e.getDisconnectCode() == SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED) {
166+
assertEquals("Unexpected disconnect reason",
167+
"SSH_MSG_DEBUG not allowed during initial key exchange in strict KEX", e.getMessage());
168+
}
169+
}
170+
}
171+
172+
@Test
173+
public void reKeyAllowsDebugInKexFromClient() throws Exception {
174+
testReKeyAllowsDebugInKex(true);
175+
}
176+
177+
@Test
178+
public void reKeyAllowsDebugInKexFromServer() throws Exception {
179+
testReKeyAllowsDebugInKex(false);
180+
}
181+
182+
private void testReKeyAllowsDebugInKex(boolean clientInitiates) throws Exception {
183+
AtomicBoolean sendDebug = new AtomicBoolean();
184+
AtomicReference<IoWriteFuture> debugMsg = new AtomicReference<>();
185+
SessionListener messageInitiator = new SessionListener() {
186+
@Override // At this stage the peer's KEX_INIT has been received
187+
public void sessionNegotiationEnd(
188+
Session session, Map<KexProposalOption, String> clientProposal,
189+
Map<KexProposalOption, String> serverProposal, Map<KexProposalOption, String> negotiatedOptions,
190+
Throwable reason) {
191+
if (sendDebug.get()) {
192+
try {
193+
debugMsg.set(session.sendDebugMessage(true, getCurrentTestName(), null));
194+
} catch (Exception e) {
195+
throw new RuntimeException(e);
196+
}
197+
}
198+
}
199+
};
200+
201+
if (clientInitiates) {
202+
client.addSessionListener(messageInitiator);
203+
} else {
204+
sshd.addSessionListener(messageInitiator);
205+
}
206+
207+
try (ClientSession session = obtainInitialTestClientSession()) {
208+
assertTrue("Session should be stablished", session.isOpen());
209+
sendDebug.set(true);
210+
assertTrue("KEX not done", session.reExchangeKeys().verify(CONNECT_TIMEOUT).isDone());
211+
IoWriteFuture future = debugMsg.get();
212+
assertNotNull("No SSH_MSG_DEBUG", future);
213+
assertTrue("SSH_MSG_DEBUG should have been sent", future.isWritten());
214+
assertTrue(session.isOpen());
215+
}
216+
}
217+
218+
@Test
219+
public void strictKexWorksWithServerFlagInClientProposal() throws Exception {
220+
testStrictKexWorksWithWrongFlag(true);
221+
}
222+
223+
@Test
224+
public void strictKexWorksWithClientFlagInServerProposal() throws Exception {
225+
testStrictKexWorksWithWrongFlag(false);
226+
}
227+
228+
private void testStrictKexWorksWithWrongFlag(boolean clientInitiates) throws Exception {
229+
SessionListener messageInitiator = new SessionListener() {
230+
@Override
231+
public void sessionNegotiationOptionsCreated(Session session, Map<KexProposalOption, String> proposal) {
232+
// Modify the proposal by including the *wrong* flag. (The framework will also add the correct flag.)
233+
String value = proposal.get(KexProposalOption.ALGORITHMS);
234+
String toAdd = clientInitiates
235+
? KexExtensions.STRICT_KEX_SERVER_EXTENSION
236+
: KexExtensions.STRICT_KEX_CLIENT_EXTENSION;
237+
if (GenericUtils.isEmpty(value)) {
238+
value = toAdd;
239+
} else {
240+
value += ',' + toAdd;
241+
}
242+
proposal.put(KexProposalOption.ALGORITHMS, value);
243+
}
244+
};
245+
246+
if (clientInitiates) {
247+
client.addSessionListener(messageInitiator);
248+
} else {
249+
sshd.addSessionListener(messageInitiator);
250+
}
251+
252+
try (ClientSession session = obtainInitialTestClientSession()) {
253+
assertTrue("Session should be stablished", session.isOpen());
254+
}
255+
}
256+
257+
private ClientSession obtainInitialTestClientSession() throws IOException {
258+
sshd.start();
259+
int port = sshd.getPort();
260+
261+
client.start();
262+
return createAuthenticatedClientSession(client, port);
263+
}
264+
}

0 commit comments

Comments
 (0)