Rework SslHandlerFactory
[netconf.git] / apps / callhome-provider / src / test / java / org / opendaylight / netconf / topology / callhome / CallHomeSshServerTest.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech s.r.o. and others. 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.topology.callhome;
9
10 import static org.mockito.ArgumentMatchers.any;
11 import static org.mockito.ArgumentMatchers.eq;
12 import static org.mockito.ArgumentMatchers.nullable;
13 import static org.mockito.Mockito.doReturn;
14 import static org.mockito.Mockito.timeout;
15 import static org.mockito.Mockito.times;
16 import static org.mockito.Mockito.verify;
17 import static org.opendaylight.netconf.api.TransportConstants.SSH_SUBSYSTEM;
18
19 import com.google.common.util.concurrent.SettableFuture;
20 import java.net.InetAddress;
21 import java.net.InetSocketAddress;
22 import java.net.ServerSocket;
23 import java.security.KeyPair;
24 import java.security.KeyPairGenerator;
25 import java.security.SecureRandom;
26 import java.security.spec.RSAKeyGenParameterSpec;
27 import java.util.List;
28 import java.util.Optional;
29 import java.util.Set;
30 import java.util.concurrent.TimeUnit;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.junit.jupiter.api.Test;
33 import org.junit.jupiter.api.extension.ExtendWith;
34 import org.mockito.Mock;
35 import org.mockito.junit.jupiter.MockitoExtension;
36 import org.opendaylight.netconf.client.NetconfClientSession;
37 import org.opendaylight.netconf.client.NetconfClientSessionListener;
38 import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory;
39 import org.opendaylight.netconf.common.impl.DefaultNetconfTimer;
40 import org.opendaylight.netconf.server.NetconfServerSession;
41 import org.opendaylight.netconf.server.NetconfServerSessionNegotiatorFactory;
42 import org.opendaylight.netconf.server.ServerTransportInitializer;
43 import org.opendaylight.netconf.server.api.monitoring.NetconfMonitoringService;
44 import org.opendaylight.netconf.server.api.monitoring.SessionListener;
45 import org.opendaylight.netconf.server.impl.DefaultSessionIdProvider;
46 import org.opendaylight.netconf.server.osgi.AggregatedNetconfOperationServiceFactory;
47 import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession;
48 import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyPairProvider;
49 import org.opendaylight.netconf.shaded.sshd.server.auth.password.PasswordAuthenticator;
50 import org.opendaylight.netconf.shaded.sshd.server.auth.password.UserAuthPasswordFactory;
51 import org.opendaylight.netconf.shaded.sshd.server.auth.pubkey.PublickeyAuthenticator;
52 import org.opendaylight.netconf.shaded.sshd.server.auth.pubkey.UserAuthPublicKeyFactory;
53 import org.opendaylight.netconf.topology.callhome.CallHomeSshAuthSettings.DefaultAuthSettings;
54 import org.opendaylight.netconf.transport.ssh.SSHServer;
55 import org.opendaylight.netconf.transport.ssh.SSHTransportStackFactory;
56 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
57 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
58 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
59 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev231228.netconf.client.initiate.stack.grouping.transport.ssh.ssh.TcpClientParametersBuilder;
60 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Capabilities;
61 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.CapabilitiesBuilder;
62 import org.opendaylight.yangtools.yang.common.Uint16;
63
64 @ExtendWith(MockitoExtension.class)
65 public class CallHomeSshServerTest {
66     private static final long TIMEOUT = 5000L;
67     private static final Capabilities EMPTY_CAPABILITIES = new CapabilitiesBuilder().setCapability(Set.of()).build();
68     private static final String USERNAME = "username";
69     private static final String PASSWORD = "pa$$W0rd";
70
71     @Mock
72     private NetconfMonitoringService monitoringService;
73     @Mock
74     private SessionListener serverSessionListener;
75     @Mock
76     private NetconfClientSessionListener clientSessionListener;
77     @Mock
78     private CallHomeStatusRecorder statusRecorder;
79
80     @Test
81     void integrationTest() throws Exception {
82
83         // key pairs
84         final var serverKeys = generateKeyPair();
85         final var client1Keys = generateKeyPair();
86         final var client2Keys = generateKeyPair();
87         final var client3Keys = generateKeyPair();
88         final var client4Keys = generateKeyPair();
89
90         // Auth provider
91         final var authProvider = (CallHomeSshAuthProvider) (remoteAddress, publicKey) -> {
92             // identify client 2 by password (invalid)
93             if (client2Keys.getPublic().equals(publicKey)) {
94                 return new DefaultAuthSettings("client2-id", USERNAME, Set.of("invalid-password"), null);
95             }
96             // identify client 3 by password (valid)
97             if (client3Keys.getPublic().equals(publicKey)) {
98                 return new DefaultAuthSettings("client3-id", USERNAME, Set.of(PASSWORD), null);
99             }
100             // identify client 4 by public key
101             if (client4Keys.getPublic().equals(publicKey)) {
102                 return new DefaultAuthSettings("client4-id", USERNAME, null, Set.of(serverKeys));
103             }
104             // client 1 is not identified
105             return null;
106         };
107         // client side authenticators
108         final PasswordAuthenticator passwordAuthenticator =
109             (username, password, session) -> USERNAME.equals(username) && PASSWORD.equals(password);
110         final PublickeyAuthenticator publicKeyAuthenticator =
111             (username, publicKey, session) -> serverKeys.getPublic().equals(publicKey);
112
113         // Netconf layer for clients
114         doReturn(serverSessionListener).when(monitoringService).getSessionListener();
115         doReturn(EMPTY_CAPABILITIES).when(monitoringService).getCapabilities();
116
117         final var timer = new DefaultNetconfTimer();
118
119         final var negotiatorFactory = NetconfServerSessionNegotiatorFactory.builder()
120             .setTimer(timer)
121             .setAggregatedOpService(new AggregatedNetconfOperationServiceFactory())
122             .setIdProvider(new DefaultSessionIdProvider())
123             .setConnectionTimeoutMillis(TIMEOUT)
124             .setMonitoringService(monitoringService)
125             .build();
126         final var netconfTransportListener = new ServerTransportInitializer(negotiatorFactory);
127
128         // tcp layer for clients
129         final var sshTransportFactory = new SSHTransportStackFactory("call-home-test-client", 0);
130         final var serverPort = serverPort();
131         final var tcpConnectParams = new TcpClientParametersBuilder()
132             .setRemoteAddress(new Host(IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress())))
133             .setRemotePort(new PortNumber(Uint16.valueOf(serverPort))).build();
134
135         // Session context manager
136         final var contextManager = new CallHomeSshSessionContextManager() {
137             // inject netconf session listener
138             @Override
139             public CallHomeSshSessionContext createContext(final String id, final ClientSession clientSession) {
140                 return new CallHomeSshSessionContext(id, clientSession.getRemoteAddress(), clientSession,
141                     clientSessionListener, SettableFuture.create());
142             }
143         };
144
145         // start Call-Home server
146         final var server = CallHomeSshServer.builder()
147             .withPort(serverPort)
148             .withAuthProvider(authProvider)
149             .withSessionContextManager(contextManager)
150             .withStatusRecorder(statusRecorder)
151             .withNegotiationFactory(new NetconfClientSessionNegotiatorFactory(timer, Optional.empty(), TIMEOUT,
152                 NetconfClientSessionNegotiatorFactory.DEFAULT_CLIENT_CAPABILITIES))
153             .build();
154
155         SSHServer client1 = null;
156         SSHServer client2 = null;
157         SSHServer client3 = null;
158         SSHServer client4 = null;
159
160         try {
161             // client 1: rejected due to public key is not identified
162             client1 = sshTransportFactory.connectServer(SSH_SUBSYSTEM, netconfTransportListener, tcpConnectParams,
163                 null, factoryMgr -> {
164                     factoryMgr.setKeyPairProvider(KeyPairProvider.wrap(client1Keys));
165                     factoryMgr.setPasswordAuthenticator(passwordAuthenticator);
166                     factoryMgr.setUserAuthFactories(List.of(new UserAuthPasswordFactory()));
167                 }).get(TIMEOUT, TimeUnit.MILLISECONDS);
168             // verify unknown key reported
169             verify(statusRecorder, timeout(TIMEOUT).times(1))
170                 .reportUnknown(any(InetSocketAddress.class), eq(client1Keys.getPublic()));
171
172             // client 2: rejected due to auth failure (wrong password)
173             client2 = sshTransportFactory.connectServer(SSH_SUBSYSTEM, netconfTransportListener, tcpConnectParams,
174                 null, factoryMgr -> {
175                     factoryMgr.setKeyPairProvider(KeyPairProvider.wrap(client2Keys));
176                     factoryMgr.setPasswordAuthenticator(passwordAuthenticator);
177                     factoryMgr.setUserAuthFactories(List.of(new UserAuthPasswordFactory()));
178                 }).get(TIMEOUT, TimeUnit.MILLISECONDS);
179             // verify auth failure reported for known key
180             verify(statusRecorder, timeout(TIMEOUT).times(1)).reportFailedAuth("client2-id");
181
182             // client 3: success with password auth
183             client3 = sshTransportFactory.connectServer(SSH_SUBSYSTEM, netconfTransportListener, tcpConnectParams,
184                 null, factoryMgr -> {
185                     factoryMgr.setKeyPairProvider(KeyPairProvider.wrap(client3Keys));
186                     factoryMgr.setPasswordAuthenticator(passwordAuthenticator);
187                     factoryMgr.setUserAuthFactories(List.of(new UserAuthPasswordFactory()));
188                 }).get(TIMEOUT, TimeUnit.MILLISECONDS);
189             // verify netconf sessions established
190             verify(clientSessionListener, timeout(TIMEOUT).times(1)).onSessionUp(any(NetconfClientSession.class));
191             verify(serverSessionListener, timeout(TIMEOUT).times(1)).onSessionUp(any(NetconfServerSession.class));
192             verify(statusRecorder, times(1)).reportSuccess("client3-id");
193
194             // client 4: success with public key auth
195             client4 = sshTransportFactory.connectServer(SSH_SUBSYSTEM, netconfTransportListener, tcpConnectParams,
196                 null, factoryMgr -> {
197                     factoryMgr.setKeyPairProvider(KeyPairProvider.wrap(client4Keys));
198                     final var pkFactory = new UserAuthPublicKeyFactory();
199                     pkFactory.setSignatureFactories(factoryMgr.getSignatureFactories());
200                     factoryMgr.setPublickeyAuthenticator(publicKeyAuthenticator);
201                     factoryMgr.setUserAuthFactories(List.of(pkFactory));
202                 }).get(TIMEOUT, TimeUnit.MILLISECONDS);
203             // verify netconf sessions established
204             verify(clientSessionListener, timeout(TIMEOUT).times(2)).onSessionUp(any(NetconfClientSession.class));
205             verify(serverSessionListener, timeout(TIMEOUT).times(2)).onSessionUp(any(NetconfServerSession.class));
206             verify(statusRecorder, times(1)).reportSuccess("client4-id");
207
208         } finally {
209             server.close();
210             shutdownClient(client1);
211             shutdownClient(client2);
212             shutdownClient(client3);
213             shutdownClient(client4);
214             timer.close();
215         }
216
217         // verify disconnect reported
218         verify(serverSessionListener, timeout(TIMEOUT).times(2)).onSessionDown(any(NetconfServerSession.class));
219         verify(clientSessionListener, timeout(TIMEOUT).times(2))
220             .onSessionDown(any(NetconfClientSession.class), nullable(Exception.class));
221         verify(statusRecorder, times(1)).reportDisconnected("client3-id");
222         verify(statusRecorder, times(1)).reportDisconnected("client4-id");
223     }
224
225     private static void shutdownClient(final @Nullable SSHServer client) throws Exception {
226         if (client != null) {
227             client.shutdown().get(TIMEOUT, TimeUnit.MILLISECONDS);
228         }
229     }
230
231     private static int serverPort() throws Exception {
232         try (var socket = new ServerSocket(0)) {
233             return socket.getLocalPort();
234         }
235     }
236
237     private static KeyPair generateKeyPair() throws Exception {
238         final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
239         keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4), new SecureRandom());
240         return keyPairGenerator.generateKeyPair();
241     }
242 }