Refactor ClientFactoryManagerConfigurator
[netconf.git] / protocol / netconf-client / src / test / java / org / opendaylight / netconf / client / NetconfClientFactoryImplTest.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.client;
9
10 import static org.junit.jupiter.api.Assertions.assertNotNull;
11 import static org.mockito.ArgumentMatchers.any;
12 import static org.mockito.Mockito.doNothing;
13 import static org.mockito.Mockito.doReturn;
14 import static org.mockito.Mockito.timeout;
15 import static org.mockito.Mockito.verify;
16
17 import io.netty.handler.ssl.SslContextBuilder;
18 import java.math.BigInteger;
19 import java.net.InetAddress;
20 import java.net.ServerSocket;
21 import java.security.KeyPairGenerator;
22 import java.security.KeyStore;
23 import java.security.SecureRandom;
24 import java.security.cert.Certificate;
25 import java.security.spec.RSAKeyGenParameterSpec;
26 import java.time.Duration;
27 import java.time.Instant;
28 import java.util.Date;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.concurrent.TimeUnit;
32 import javax.net.ssl.KeyManagerFactory;
33 import javax.net.ssl.TrustManagerFactory;
34 import org.bouncycastle.asn1.x500.X500Name;
35 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
36 import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
37 import org.bouncycastle.jce.provider.BouncyCastleProvider;
38 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
39 import org.junit.jupiter.api.AfterAll;
40 import org.junit.jupiter.api.AfterEach;
41 import org.junit.jupiter.api.BeforeAll;
42 import org.junit.jupiter.api.BeforeEach;
43 import org.junit.jupiter.api.Test;
44 import org.junit.jupiter.api.extension.ExtendWith;
45 import org.mockito.Mock;
46 import org.mockito.junit.jupiter.MockitoExtension;
47 import org.opendaylight.netconf.client.conf.NetconfClientConfiguration;
48 import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder;
49 import org.opendaylight.netconf.common.impl.DefaultNetconfTimer;
50 import org.opendaylight.netconf.shaded.sshd.client.ClientFactoryManager;
51 import org.opendaylight.netconf.shaded.sshd.client.auth.password.PasswordIdentityProvider;
52 import org.opendaylight.netconf.shaded.sshd.server.auth.password.UserAuthPasswordFactory;
53 import org.opendaylight.netconf.shaded.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
54 import org.opendaylight.netconf.transport.api.TransportChannel;
55 import org.opendaylight.netconf.transport.api.TransportChannelListener;
56 import org.opendaylight.netconf.transport.ssh.ClientFactoryManagerConfigurator;
57 import org.opendaylight.netconf.transport.ssh.SSHTransportStackFactory;
58 import org.opendaylight.netconf.transport.ssh.ServerFactoryManagerConfigurator;
59 import org.opendaylight.netconf.transport.tcp.TCPServer;
60 import org.opendaylight.netconf.transport.tls.FixedSslHandlerFactory;
61 import org.opendaylight.netconf.transport.tls.TLSServer;
62 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.crypt.hash.rev140806.CryptHash;
63 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev240208.RsaPrivateKeyFormat;
64 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev240208.SubjectPublicKeyInfoFormat;
65 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev240208._private.key.grouping._private.key.type.CleartextPrivateKeyBuilder;
66 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev240208.password.grouping.password.type.CleartextPasswordBuilder;
67 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
68 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
69 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
70 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev240208.inline.or.keystore.asymmetric.key.grouping.inline.or.keystore.InlineBuilder;
71 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev240208.inline.or.keystore.asymmetric.key.grouping.inline.or.keystore.inline.InlineDefinitionBuilder;
72 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev240208.netconf.client.initiate.stack.grouping.transport.tls.tls.TcpClientParametersBuilder;
73 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev240208.netconf.client.listen.stack.grouping.transport.ssh.ssh.SshClientParametersBuilder;
74 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev240208.netconf.client.listen.stack.grouping.transport.ssh.ssh.TcpServerParametersBuilder;
75 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev240208.ssh.client.grouping.ClientIdentityBuilder;
76 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev240208.ssh.client.grouping.client.identity.PasswordBuilder;
77 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208.SshServerGrouping;
78 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208.ssh.server.grouping.ClientAuthentication;
79 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208.ssh.server.grouping.ClientAuthenticationBuilder;
80 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208.ssh.server.grouping.ServerIdentity;
81 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208.ssh.server.grouping.ServerIdentityBuilder;
82 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208.ssh.server.grouping.client.authentication.UsersBuilder;
83 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208.ssh.server.grouping.client.authentication.users.UserBuilder;
84 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208.ssh.server.grouping.server.identity.HostKeyBuilder;
85 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208.ssh.server.grouping.server.identity.host.key.host.key.type.PublicKeyBuilder;
86 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev240208.TcpClientGrouping;
87 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev240208.TcpServerGrouping;
88 import org.opendaylight.yangtools.yang.common.Uint16;
89
90 @ExtendWith(MockitoExtension.class)
91 class NetconfClientFactoryImplTest {
92     private static final String USERNAME = "username";
93     private static final String PASSWORD = "pa$$w0rd";
94     private static final String RSA = "RSA";
95     private static final char[] EMPTY_SECRET = new char[0];
96
97     private static SSHTransportStackFactory SERVER_FACTORY;
98     private static DefaultNetconfTimer TIMER;
99
100     @Mock
101     private NetconfClientSessionListener sessionListener;
102     @Mock
103     private TransportChannelListener serverTransportListener;
104     @Mock
105     private SshServerGrouping sshServerParams;
106
107     private NetconfClientFactory factory;
108     private TcpServerGrouping tcpServerParams;
109     private TcpClientGrouping tcpClientParams;
110
111     @BeforeAll
112     static void beforeAll() {
113         SERVER_FACTORY = new SSHTransportStackFactory("server", 0);
114         TIMER = new DefaultNetconfTimer();
115     }
116
117     @AfterAll
118     static void afterAll() {
119         SERVER_FACTORY.close();
120         TIMER.close();
121     }
122
123     @BeforeEach
124     void beforeEach() throws Exception {
125         factory = new NetconfClientFactoryImpl(TIMER);
126         doNothing().when(serverTransportListener).onTransportChannelEstablished(any());
127
128         // create temp socket to get available port for test
129         final var socket = new ServerSocket(0);
130         final var address = IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress());
131         final var port = new PortNumber(Uint16.valueOf(socket.getLocalPort()));
132         socket.close();
133
134         tcpServerParams = new TcpServerParametersBuilder().setLocalAddress(address).setLocalPort(port).build();
135         tcpClientParams =
136             new TcpClientParametersBuilder().setRemoteAddress(new Host(address)).setRemotePort(port).build();
137     }
138
139     @AfterEach
140     void afterEach() throws Exception {
141         if (factory != null) {
142             factory.close();
143         }
144     }
145
146     @Test
147     void tcpClient() throws Exception {
148         final var server = TCPServer.listen(serverTransportListener,
149             SERVER_FACTORY.newServerBootstrap(), tcpServerParams).get(1, TimeUnit.SECONDS);
150         try {
151             final var clientConfig = NetconfClientConfigurationBuilder.create()
152                 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TCP)
153                 .withTcpParameters(tcpClientParams).withSessionListener(sessionListener).build();
154             assertNotNull(factory.createClient(clientConfig));
155             verify(serverTransportListener, timeout(1000L))
156                 .onTransportChannelEstablished(any(TransportChannel.class));
157         } finally {
158             server.shutdown().get(1, TimeUnit.SECONDS);
159         }
160     }
161
162     @Test
163     void tlsClient() throws Exception {
164         final var keyStore = buildKeystoreWithGeneratedCertificate();
165         final var keyMgr = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
166         keyMgr.init(keyStore, EMPTY_SECRET);
167         final var trustMgr = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
168         trustMgr.init(keyStore);
169         final var serverContext = SslContextBuilder.forServer(keyMgr).trustManager(trustMgr).build();
170         final var clientContext = SslContextBuilder.forClient().keyManager(keyMgr).trustManager(trustMgr).build();
171
172         final var server = TLSServer.listen(serverTransportListener, SERVER_FACTORY.newServerBootstrap(),
173             tcpServerParams, new FixedSslHandlerFactory(serverContext)).get(1, TimeUnit.SECONDS);
174         try {
175             final var clientConfig = NetconfClientConfigurationBuilder.create()
176                 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TLS)
177                 .withTcpParameters(tcpClientParams)
178                 .withSslHandlerFactory(new FixedSslHandlerFactory(clientContext))
179                 .withSessionListener(sessionListener).build();
180             assertNotNull(factory.createClient(clientConfig));
181             verify(serverTransportListener, timeout(1000L))
182                 .onTransportChannelEstablished(any(TransportChannel.class));
183         } finally {
184             server.shutdown().get(1, TimeUnit.SECONDS);
185         }
186     }
187
188     private static KeyStore buildKeystoreWithGeneratedCertificate() throws Exception {
189         // key pair
190         final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
191         keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4), new SecureRandom());
192         final var keyPair = keyPairGenerator.generateKeyPair();
193         // certificate
194         final var now = Instant.now();
195         final var contentSigner = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
196         final var x500Name = new X500Name("CN=TestCertificate");
197         final var certificateBuilder = new JcaX509v3CertificateBuilder(x500Name,
198             BigInteger.valueOf(now.toEpochMilli()),
199             Date.from(now), Date.from(now.plus(Duration.ofDays(365))),
200             x500Name,
201             keyPair.getPublic());
202         final var certificate = new JcaX509CertificateConverter()
203             .setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner));
204         // keystore with certificate and key
205         final var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
206         keyStore.load(null, null);
207         keyStore.setCertificateEntry("cert", certificate);
208         keyStore.setKeyEntry("key", keyPair.getPrivate(), EMPTY_SECRET, new Certificate[]{certificate});
209         return keyStore;
210     }
211
212     @Test
213     void sshClient() throws Exception {
214         doReturn(buildSshServerIdentity()).when(sshServerParams).getServerIdentity();
215         doReturn(buildSshClientAuth()).when(sshServerParams).getClientAuthentication();
216         doReturn(null).when(sshServerParams).getTransportParams();
217         doReturn(null).when(sshServerParams).getKeepalives();
218
219         final var server = SERVER_FACTORY.listenServer("netconf", serverTransportListener, tcpServerParams,
220             sshServerParams).get(10, TimeUnit.SECONDS);
221
222         try {
223             final var clientConfig = NetconfClientConfigurationBuilder.create()
224                 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.SSH)
225                 .withTcpParameters(tcpClientParams)
226                 .withSshParameters(new SshClientParametersBuilder()
227                     .setClientIdentity(new ClientIdentityBuilder().setUsername(USERNAME)
228                         .setPassword(new PasswordBuilder().setPasswordType(
229                             new CleartextPasswordBuilder().setCleartextPassword(PASSWORD)
230                                 .build()).build()).build())
231                     .build())
232                 .withSessionListener(sessionListener)
233                 .withConnectionTimeoutMillis(10_000)
234                 .build();
235             assertNotNull(factory.createClient(clientConfig));
236             verify(serverTransportListener, timeout(10_000L))
237                 .onTransportChannelEstablished(any(TransportChannel.class));
238         } finally {
239             server.shutdown().get(1, TimeUnit.SECONDS);
240         }
241     }
242
243     private static ServerIdentity buildSshServerIdentity() throws Exception {
244         final var keyPair = KeyPairGenerator.getInstance(RSA).generateKeyPair();
245         final var inlineDef = new InlineDefinitionBuilder()
246             .setPublicKeyFormat(SubjectPublicKeyInfoFormat.VALUE)
247             .setPublicKey(keyPair.getPublic().getEncoded())
248             .setPrivateKeyFormat(RsaPrivateKeyFormat.VALUE)
249             .setPrivateKeyType(
250                 new CleartextPrivateKeyBuilder().setCleartextPrivateKey(
251                     keyPair.getPrivate().getEncoded()
252                 ).build()
253             ).build();
254         final var inline = new InlineBuilder().setInlineDefinition(inlineDef).build();
255         final var publicKey = new PublicKeyBuilder().setPublicKey(
256             new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208
257                 .ssh.server.grouping.server.identity.host.key.host.key.type._public.key
258                 .PublicKeyBuilder().setInlineOrKeystore(inline).build()
259         ).build();
260         return new ServerIdentityBuilder().setHostKey(
261             List.of(new HostKeyBuilder().setName("test-name").setHostKeyType(publicKey).build())
262         ).build();
263     }
264
265     private static ClientAuthentication buildSshClientAuth() {
266         final var user = new UserBuilder().setName(USERNAME)
267             .setPassword(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev240208.ssh
268                 .server.grouping.client.authentication.users.user.PasswordBuilder()
269                 .setHashedPassword(new CryptHash("$0$" + PASSWORD))
270                 .build())
271             .build();
272         return new ClientAuthenticationBuilder().setUsers(
273             new UsersBuilder().setUser(Map.of(user.key(), user)).build()
274         ).build();
275     }
276
277     @Test
278     void sshClientWithConfigurator() throws Exception {
279         final ServerFactoryManagerConfigurator serverConfigurator = factoryManager -> {
280             factoryManager.setUserAuthFactories(List.of(new UserAuthPasswordFactory()));
281             factoryManager.setPasswordAuthenticator(
282                 (usr, psw, session) -> USERNAME.equals(usr) && PASSWORD.equals(psw));
283             factoryManager.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
284         };
285         final var clientConfigurator = new ClientFactoryManagerConfigurator() {
286             @Override
287             protected void configureClientFactoryManager(final ClientFactoryManager factoryManager) {
288                 factoryManager.setPasswordIdentityProvider(PasswordIdentityProvider.wrapPasswords(PASSWORD));
289                 factoryManager.setUserAuthFactories(List.of(
290                     new org.opendaylight.netconf.shaded.sshd.client.auth.password.UserAuthPasswordFactory()));
291             }
292         };
293
294         final var server = SERVER_FACTORY.listenServer("netconf", serverTransportListener, tcpServerParams,
295             null, serverConfigurator).get(10, TimeUnit.SECONDS);
296         try {
297             final var clientConfig = NetconfClientConfigurationBuilder.create()
298                 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.SSH)
299                 .withTcpParameters(tcpClientParams)
300                 .withSshParameters(new SshClientParametersBuilder()
301                     .setClientIdentity(new ClientIdentityBuilder().setUsername(USERNAME).build()).build())
302                 .withSshConfigurator(clientConfigurator)
303                 .withSessionListener(sessionListener)
304                 .withConnectionTimeoutMillis(10_000)
305                 .build();
306             assertNotNull(factory.createClient(clientConfig));
307             verify(serverTransportListener, timeout(10_000L))
308                 .onTransportChannelEstablished(any(TransportChannel.class));
309         } finally {
310             server.shutdown().get(1, TimeUnit.SECONDS);
311         }
312     }
313 }