Bump upstreams
[netconf.git] / transport / transport-ssh / src / test / java / org / opendaylight / netconf / transport / ssh / SshClientServerTest.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech s.r.o. All rights reserved.
3  *
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
7  */
8 package org.opendaylight.netconf.transport.ssh;
9
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;
28
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;
70
71 @ExtendWith(MockitoExtension.class)
72 public class SshClientServerTest {
73
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);
80
81     @Mock
82     private TcpClientGrouping tcpClientConfig;
83     @Mock
84     private SshClientGrouping sshClientConfig;
85     @Mock
86     private TransportChannelListener clientListener;
87     @Mock
88     private TcpServerGrouping tcpServerConfig;
89     @Mock
90     private SshServerGrouping sshServerConfig;
91     @Mock
92     private TransportChannelListener serverListener;
93
94     @Captor
95     ArgumentCaptor<TransportChannel> clientTransportChannelCaptor;
96     @Captor
97     ArgumentCaptor<TransportChannel> serverTransportChannelCaptor;
98
99     private static EventLoopGroup group;
100     private ServerSocket socket;
101
102     @BeforeAll
103     static void beforeAll() throws Exception {
104         group = NettyTransportSupport.newEventLoopGroup("IntegrationTest");
105     }
106
107     @AfterAll
108     static void afterAll() {
109         group.shutdownGracefully();
110         group = null;
111     }
112
113     @BeforeEach
114     void beforeEach() throws IOException {
115
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()));
120         socket.close();
121
122         when(tcpServerConfig.getLocalAddress()).thenReturn(localAddress);
123         when(tcpServerConfig.requireLocalAddress()).thenCallRealMethod();
124         when(tcpServerConfig.getLocalPort()).thenReturn(localPort);
125         when(tcpServerConfig.requireLocalPort()).thenCallRealMethod();
126
127         when(tcpClientConfig.getRemoteAddress()).thenReturn(new Host(localAddress));
128         when(tcpClientConfig.requireRemoteAddress()).thenCallRealMethod();
129         when(tcpClientConfig.getRemotePort()).thenReturn(localPort);
130         when(tcpClientConfig.requireRemotePort()).thenCallRealMethod();
131     }
132
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);
143         integrationTest();
144     }
145
146     private static Stream<Arguments> itServerKeyVerifyArgs() throws Exception {
147         final var rsaKeyData = generateKeyPairWithCertificate(RSA);
148         final var ecKeyData = generateKeyPairWithCertificate(EC);
149         return Stream.of(
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))
158         );
159     }
160
161     @ParameterizedTest(name = "SSH User Auth using {0}")
162     @MethodSource("itUserAuthArgs")
163     void itUserAuth(final String testDesc, final ClientIdentity clientIdentity, final ClientAuthentication clientAuth)
164             throws Exception {
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);
170         integrationTest();
171     }
172
173     private static Stream<Arguments> itUserAuthArgs() throws Exception {
174         final var rsaKeyData = generateKeyPairWithCertificate(RSA);
175         final var ecKeyData = generateKeyPairWithCertificate(EC);
176         return Stream.of(
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))
203         );
204     }
205
206     private static String getUsername() {
207         return USERNAME.get();
208     }
209
210     /**
211      * Update username for next test.
212      */
213     private static String getUsernameAndUpdate() {
214         return USERNAME.getAndSet(USER + COUNTER.incrementAndGet());
215     }
216
217     private void integrationTest() throws Exception {
218         // start server
219         final var server = SSHServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group),
220                 tcpServerConfig, sshServerConfig).get(2, TimeUnit.SECONDS);
221         try {
222             // connect with client
223             final var client = SSHClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
224                     tcpClientConfig, sshClientConfig).get(2, TimeUnit.SECONDS);
225             try {
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());
239
240             } finally {
241                 client.shutdown().get(2, TimeUnit.SECONDS);
242             }
243         } finally {
244             server.shutdown().get(2, TimeUnit.SECONDS);
245         }
246     }
247
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
254         return channel;
255     }
256
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());
262     }
263 }