2 * Copyright (c) 2023 PANTHEON.tech s.r.o. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.netconf.transport.ssh;
10 import static org.junit.jupiter.api.Assertions.assertEquals;
11 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
12 import static org.junit.jupiter.api.Assertions.assertNotNull;
13 import static org.junit.jupiter.api.Assertions.assertTrue;
14 import static org.mockito.Mockito.timeout;
15 import static org.mockito.Mockito.verify;
16 import static org.mockito.Mockito.when;
17 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildClientAuthHostBased;
18 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildClientAuthWithPassword;
19 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildClientAuthWithPublicKey;
20 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildClientIdentityHostBased;
21 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildClientIdentityWithPassword;
22 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildClientIdentityWithPublicKey;
23 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildServerAuthWithCertificate;
24 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildServerAuthWithPublicKey;
25 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildServerIdentityWithCertificate;
26 import static org.opendaylight.netconf.transport.ssh.TestUtils.buildServerIdentityWithKeyPair;
27 import static org.opendaylight.netconf.transport.ssh.TestUtils.generateKeyPairWithCertificate;
29 import io.netty.channel.Channel;
30 import io.netty.channel.EventLoopGroup;
31 import java.io.IOException;
32 import java.net.InetAddress;
33 import java.net.ServerSocket;
34 import java.util.Collection;
35 import java.util.List;
36 import java.util.concurrent.TimeUnit;
37 import java.util.concurrent.atomic.AtomicInteger;
38 import java.util.concurrent.atomic.AtomicReference;
39 import java.util.stream.Stream;
40 import org.apache.commons.codec.digest.Crypt;
41 import org.junit.jupiter.api.AfterAll;
42 import org.junit.jupiter.api.BeforeAll;
43 import org.junit.jupiter.api.BeforeEach;
44 import org.junit.jupiter.api.extension.ExtendWith;
45 import org.junit.jupiter.params.ParameterizedTest;
46 import org.junit.jupiter.params.provider.Arguments;
47 import org.junit.jupiter.params.provider.MethodSource;
48 import org.mockito.ArgumentCaptor;
49 import org.mockito.Captor;
50 import org.mockito.Mock;
51 import org.mockito.junit.jupiter.MockitoExtension;
52 import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession;
53 import org.opendaylight.netconf.shaded.sshd.common.session.Session;
54 import org.opendaylight.netconf.shaded.sshd.server.session.ServerSession;
55 import org.opendaylight.netconf.transport.api.TransportChannel;
56 import org.opendaylight.netconf.transport.api.TransportChannelListener;
57 import org.opendaylight.netconf.transport.tcp.NettyTransportSupport;
58 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
59 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
60 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
61 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev221212.SshClientGrouping;
62 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev221212.ssh.client.grouping.ClientIdentity;
63 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev221212.ssh.client.grouping.ServerAuthentication;
64 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev221212.SshServerGrouping;
65 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev221212.ssh.server.grouping.ClientAuthentication;
66 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev221212.ssh.server.grouping.ServerIdentity;
67 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev221212.TcpClientGrouping;
68 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev221212.TcpServerGrouping;
69 import org.opendaylight.yangtools.yang.common.Uint16;
71 @ExtendWith(MockitoExtension.class)
72 public class SshClientServerTest {
74 private static final String RSA = "RSA";
75 private static final String EC = "EC";
76 private static final String USER = "user";
77 private static final String PASSWORD = "pa$$w0rd";
78 private static final AtomicInteger COUNTER = new AtomicInteger(0);
79 private static final AtomicReference<String> USERNAME = new AtomicReference<>(USER);
82 private TcpClientGrouping tcpClientConfig;
84 private SshClientGrouping sshClientConfig;
86 private TransportChannelListener clientListener;
88 private TcpServerGrouping tcpServerConfig;
90 private SshServerGrouping sshServerConfig;
92 private TransportChannelListener serverListener;
95 ArgumentCaptor<TransportChannel> clientTransportChannelCaptor;
97 ArgumentCaptor<TransportChannel> serverTransportChannelCaptor;
99 private static EventLoopGroup group;
100 private ServerSocket socket;
103 static void beforeAll() throws Exception {
104 group = NettyTransportSupport.newEventLoopGroup("IntegrationTest");
108 static void afterAll() {
109 group.shutdownGracefully();
114 void beforeEach() throws IOException {
116 // create temp socket to get available port for test
117 socket = new ServerSocket(0);
118 final var localAddress = IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress());
119 final var localPort = new PortNumber(Uint16.valueOf(socket.getLocalPort()));
122 when(tcpServerConfig.getLocalAddress()).thenReturn(localAddress);
123 when(tcpServerConfig.requireLocalAddress()).thenCallRealMethod();
124 when(tcpServerConfig.getLocalPort()).thenReturn(localPort);
125 when(tcpServerConfig.requireLocalPort()).thenCallRealMethod();
127 when(tcpClientConfig.getRemoteAddress()).thenReturn(new Host(localAddress));
128 when(tcpClientConfig.requireRemoteAddress()).thenCallRealMethod();
129 when(tcpClientConfig.getRemotePort()).thenReturn(localPort);
130 when(tcpClientConfig.requireRemotePort()).thenCallRealMethod();
133 @ParameterizedTest(name = "SSH Server Host Key Verification -- {0}")
134 @MethodSource("itServerKeyVerifyArgs")
135 void itServerKeyVerify(final String testDesc, final ServerIdentity serverIdentity,
136 final ServerAuthentication serverAuth) throws Exception {
137 final var clientIdentity = buildClientIdentityWithPassword(getUsername(), PASSWORD);
138 final var clientAuth = buildClientAuthWithPassword(getUsernameAndUpdate(), "$0$" + PASSWORD);
139 when(sshClientConfig.getClientIdentity()).thenReturn(clientIdentity);
140 when(sshClientConfig.getServerAuthentication()).thenReturn(serverAuth);
141 when(sshServerConfig.getServerIdentity()).thenReturn(serverIdentity);
142 when(sshServerConfig.getClientAuthentication()).thenReturn(clientAuth);
146 private static Stream<Arguments> itServerKeyVerifyArgs() throws Exception {
147 final var rsaKeyData = generateKeyPairWithCertificate(RSA);
148 final var ecKeyData = generateKeyPairWithCertificate(EC);
150 Arguments.of("RSA public key",
151 buildServerIdentityWithKeyPair(rsaKeyData), buildServerAuthWithPublicKey(rsaKeyData)),
152 Arguments.of("EC public key",
153 buildServerIdentityWithKeyPair(ecKeyData), buildServerAuthWithPublicKey(ecKeyData)),
154 Arguments.of("RSA certificate",
155 buildServerIdentityWithCertificate(rsaKeyData), buildServerAuthWithCertificate(rsaKeyData)),
156 Arguments.of("EC certificate",
157 buildServerIdentityWithCertificate(ecKeyData), buildServerAuthWithCertificate(ecKeyData))
161 @ParameterizedTest(name = "SSH User Auth using {0}")
162 @MethodSource("itUserAuthArgs")
163 void itUserAuth(final String testDesc, final ClientIdentity clientIdentity, final ClientAuthentication clientAuth)
165 final var serverIdentity = buildServerIdentityWithKeyPair(generateKeyPairWithCertificate(RSA)); // required
166 when(sshClientConfig.getClientIdentity()).thenReturn(clientIdentity);
167 when(sshClientConfig.getServerAuthentication()).thenReturn(null); // Accept all keys
168 when(sshServerConfig.getServerIdentity()).thenReturn(serverIdentity);
169 when(sshServerConfig.getClientAuthentication()).thenReturn(clientAuth);
173 private static Stream<Arguments> itUserAuthArgs() throws Exception {
174 final var rsaKeyData = generateKeyPairWithCertificate(RSA);
175 final var ecKeyData = generateKeyPairWithCertificate(EC);
177 Arguments.of("Password -- clear text ",
178 buildClientIdentityWithPassword(getUsername(), PASSWORD),
179 buildClientAuthWithPassword(getUsernameAndUpdate(), "$0$" + PASSWORD)),
180 Arguments.of("Password -- MD5",
181 buildClientIdentityWithPassword(getUsername(), PASSWORD),
182 buildClientAuthWithPassword(getUsernameAndUpdate(), Crypt.crypt(PASSWORD, "$1$md5salt"))),
183 Arguments.of("Password -- SHA-256",
184 buildClientIdentityWithPassword(getUsername(), PASSWORD),
185 buildClientAuthWithPassword(getUsernameAndUpdate(),
186 Crypt.crypt(PASSWORD, "$5$sha256salt"))),
187 Arguments.of("Password -- SHA-512 with rounds",
188 buildClientIdentityWithPassword(getUsername(), PASSWORD),
189 buildClientAuthWithPassword(getUsernameAndUpdate(),
190 Crypt.crypt(PASSWORD, "$6$rounds=4500$sha512salt"))),
191 Arguments.of("HostBased -- RSA keys",
192 buildClientIdentityHostBased(getUsername(), rsaKeyData),
193 buildClientAuthHostBased(getUsernameAndUpdate(), rsaKeyData)),
194 Arguments.of("HostBased -- EC keys",
195 buildClientIdentityHostBased(getUsername(), ecKeyData),
196 buildClientAuthHostBased(getUsernameAndUpdate(), ecKeyData)),
197 Arguments.of("PublicKey -- RSA keys",
198 buildClientIdentityWithPublicKey(getUsername(), rsaKeyData),
199 buildClientAuthWithPublicKey(getUsernameAndUpdate(), rsaKeyData)),
200 Arguments.of("PublicBased -- EC keys",
201 buildClientIdentityWithPublicKey(getUsername(), ecKeyData),
202 buildClientAuthWithPublicKey(getUsernameAndUpdate(), ecKeyData))
206 private static String getUsername() {
207 return USERNAME.get();
211 * Update username for next test.
213 private static String getUsernameAndUpdate() {
214 return USERNAME.getAndSet(USER + COUNTER.incrementAndGet());
217 private void integrationTest() throws Exception {
219 final var server = SSHServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group),
220 tcpServerConfig, sshServerConfig).get(2, TimeUnit.SECONDS);
222 // connect with client
223 final var client = SSHClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
224 tcpClientConfig, sshClientConfig).get(2, TimeUnit.SECONDS);
226 verify(serverListener, timeout(10_000))
227 .onTransportChannelEstablished(serverTransportChannelCaptor.capture());
228 verify(clientListener, timeout(10_000))
229 .onTransportChannelEstablished(clientTransportChannelCaptor.capture());
230 // validate channels are in expected state
231 var serverChannel = assertChannel(serverTransportChannelCaptor.getAllValues());
232 var clientChannel = assertChannel(clientTransportChannelCaptor.getAllValues());
233 // validate channels are connecting same sockets
234 assertEquals(serverChannel.remoteAddress(), clientChannel.localAddress());
235 assertEquals(serverChannel.localAddress(), clientChannel.remoteAddress());
236 // validate sessions are authenticated
237 assertSession(ServerSession.class, server.getSessions());
238 assertSession(ClientSession.class, client.getSessions());
241 client.shutdown().get(2, TimeUnit.SECONDS);
244 server.shutdown().get(2, TimeUnit.SECONDS);
248 private static Channel assertChannel(List<TransportChannel> transportChannels) {
249 assertNotNull(transportChannels);
250 assertEquals(1, transportChannels.size());
251 final var channel = assertInstanceOf(SSHTransportChannel.class, transportChannels.get(0)).channel();
252 assertNotNull(channel);
253 assertTrue(channel.isOpen()); // connection is open
257 private static <T extends Session> void assertSession(Class<T> type, Collection<Session> sessions) {
258 assertNotNull(sessions);
259 assertEquals(1, sessions.size());
260 final T session = assertInstanceOf(type, sessions.iterator().next());
261 assertTrue(session.isAuthenticated());