Skip to content

Commit a5e76c6

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 d67b6f5 commit a5e76c6

File tree

1 file changed

+219
-0
lines changed

1 file changed

+219
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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.server.SshServer;
36+
import org.apache.sshd.util.test.BaseTestSupport;
37+
import org.junit.After;
38+
import org.junit.Before;
39+
import org.junit.FixMethodOrder;
40+
import org.junit.Test;
41+
import org.junit.runners.MethodSorters;
42+
43+
/**
44+
* Tests for message handling during "strict KEX" is active: initial KEX must fail and disconnect if the KEX_INIT
45+
* message is not first, or if there are spurious extra messages like IGNORE or DEBUG during KEX. Later KEXes must
46+
* succeed even if there are spurious messages.
47+
* <p>
48+
* The other part of "strict KEX" is resetting the message sequence numbers after KEX. This is not tested here but in
49+
* the {@link StrictKexInteroperabilityTest}, which runs an Apache MINA sshd client against OpenSSH servers that have or
50+
* do not have the "strict KEX" extension. If the sequence number handling was wrong, those tests would fail.
51+
* </p>
52+
*
53+
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
54+
* @see <A HREF="https://github.com/apache/mina-sshd/issues/445">Terrapin Mitigation: &quot;strict-kex&quot;</A>
55+
*/
56+
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
57+
public class StrictKexTest extends BaseTestSupport {
58+
private SshServer sshd;
59+
private SshClient client;
60+
61+
public StrictKexTest() {
62+
super();
63+
}
64+
65+
@Before
66+
public void setUp() throws Exception {
67+
sshd = setupTestServer();
68+
client = setupTestClient();
69+
}
70+
71+
@After
72+
public void tearDown() throws Exception {
73+
if (sshd != null) {
74+
sshd.stop(true);
75+
}
76+
if (client != null) {
77+
client.stop();
78+
}
79+
}
80+
81+
@Test
82+
public void testConnectionClosedIfFirstPacketFromClientNotKexInit() throws Exception {
83+
testConnectionClosedIfFirstPacketFromPeerNotKexInit(true);
84+
}
85+
86+
@Test
87+
public void testConnectionClosedIfFirstPacketFromServerNotKexInit() throws Exception {
88+
testConnectionClosedIfFirstPacketFromPeerNotKexInit(false);
89+
}
90+
91+
private void testConnectionClosedIfFirstPacketFromPeerNotKexInit(boolean clientInitiates) throws Exception {
92+
AtomicReference<IoWriteFuture> debugMsg = new AtomicReference<>();
93+
SessionListener messageInitiator = new SessionListener() {
94+
@Override // At this stage KEX-INIT not sent yet
95+
public void sessionNegotiationOptionsCreated(Session session, Map<KexProposalOption, String> proposal) {
96+
try {
97+
debugMsg.set(session.sendDebugMessage(true, getCurrentTestName(), null));
98+
} catch (Exception e) {
99+
throw new RuntimeException(e);
100+
}
101+
}
102+
};
103+
104+
if (clientInitiates) {
105+
client.addSessionListener(messageInitiator);
106+
} else {
107+
sshd.addSessionListener(messageInitiator);
108+
}
109+
110+
try (ClientSession session = obtainInitialTestClientSession()) {
111+
fail("Unexpected session success");
112+
} catch (SshException e) {
113+
IoWriteFuture future = debugMsg.get();
114+
assertNotNull("No SSH_MSG_DEBUG", future);
115+
assertTrue("SSH_MSG_DEBUG should have been sent", future.isWritten());
116+
assertEquals("Unexpected disconnect code", SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, e.getDisconnectCode());
117+
assertTrue("Unexpected disconnect reason: " + e.getMessage(),
118+
e.getMessage().startsWith("Strict KEX negotiated but sequence number of first KEX_INIT received is not 1"));
119+
}
120+
}
121+
122+
@Test
123+
public void testConnectionClosedIfSpuriousPacketFromClientInKex() throws Exception {
124+
testConnectionClosedIfSupriousPacketInKex(true);
125+
}
126+
127+
@Test
128+
public void testConnectionClosedIfSpuriousPacketFromServerInKex() throws Exception {
129+
testConnectionClosedIfSupriousPacketInKex(false);
130+
}
131+
132+
private void testConnectionClosedIfSupriousPacketInKex(boolean clientInitiates) throws Exception {
133+
AtomicReference<IoWriteFuture> debugMsg = new AtomicReference<>();
134+
SessionListener messageInitiator = new SessionListener() {
135+
@Override // At this stage the peer's KEX_INIT has been received
136+
public void sessionNegotiationEnd(
137+
Session session, Map<KexProposalOption, String> clientProposal,
138+
Map<KexProposalOption, String> serverProposal, Map<KexProposalOption, String> negotiatedOptions,
139+
Throwable reason) {
140+
try {
141+
debugMsg.set(session.sendDebugMessage(true, getCurrentTestName(), null));
142+
} catch (Exception e) {
143+
throw new RuntimeException(e);
144+
}
145+
}
146+
};
147+
148+
if (clientInitiates) {
149+
client.addSessionListener(messageInitiator);
150+
} else {
151+
sshd.addSessionListener(messageInitiator);
152+
}
153+
154+
try (ClientSession session = obtainInitialTestClientSession()) {
155+
fail("Unexpected session success");
156+
} catch (SshException e) {
157+
IoWriteFuture future = debugMsg.get();
158+
assertNotNull("No SSH_MSG_DEBUG", future);
159+
assertTrue("SSH_MSG_DEBUG should have been sent", future.isWritten());
160+
assertEquals("Unexpected disconnect code", SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, e.getDisconnectCode());
161+
assertEquals("Unexpected disconnect reason", "SSH_MSG_DEBUG not allowed during initial key exchange in strict KEX",
162+
e.getMessage());
163+
}
164+
}
165+
166+
@Test
167+
public void testReKeyAllowsDebugInKexFromClient() throws Exception {
168+
testReKeyAllowsDebugInKex(true);
169+
}
170+
171+
@Test
172+
public void testReKeyAllowsDebugInKexFromServer() throws Exception {
173+
testReKeyAllowsDebugInKex(false);
174+
}
175+
176+
private void testReKeyAllowsDebugInKex(boolean clientInitiates) throws Exception {
177+
AtomicBoolean sendDebug = new AtomicBoolean();
178+
AtomicReference<IoWriteFuture> debugMsg = new AtomicReference<>();
179+
SessionListener messageInitiator = new SessionListener() {
180+
@Override // At this stage the peer's KEX_INIT has been received
181+
public void sessionNegotiationEnd(
182+
Session session, Map<KexProposalOption, String> clientProposal,
183+
Map<KexProposalOption, String> serverProposal, Map<KexProposalOption, String> negotiatedOptions,
184+
Throwable reason) {
185+
if (sendDebug.get()) {
186+
try {
187+
debugMsg.set(session.sendDebugMessage(true, getCurrentTestName(), null));
188+
} catch (Exception e) {
189+
throw new RuntimeException(e);
190+
}
191+
}
192+
}
193+
};
194+
195+
if (clientInitiates) {
196+
client.addSessionListener(messageInitiator);
197+
} else {
198+
sshd.addSessionListener(messageInitiator);
199+
}
200+
201+
try (ClientSession session = obtainInitialTestClientSession()) {
202+
assertTrue("Session should be stablished", session.isOpen());
203+
sendDebug.set(true);
204+
assertTrue("KEX not done", session.reExchangeKeys().verify(CONNECT_TIMEOUT).isDone());
205+
IoWriteFuture future = debugMsg.get();
206+
assertNotNull("No SSH_MSG_DEBUG", future);
207+
assertTrue("SSH_MSG_DEBUG should have been sent", future.isWritten());
208+
assertTrue(session.isOpen());
209+
}
210+
}
211+
212+
private ClientSession obtainInitialTestClientSession() throws IOException {
213+
sshd.start();
214+
int port = sshd.getPort();
215+
216+
client.start();
217+
return createAuthenticatedClientSession(client, port);
218+
}
219+
}

0 commit comments

Comments
 (0)