ea8436b68d931f526b3f896b4022c837e42a1b24
[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.transport.api.TransportChannel;
50 import org.opendaylight.netconf.transport.api.TransportChannelListener;
51 import org.opendaylight.netconf.transport.ssh.SSHTransportStackFactory;
52 import org.opendaylight.netconf.transport.tcp.TCPServer;
53 import org.opendaylight.netconf.transport.tls.TLSServer;
54 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.crypt.hash.rev140806.CryptHash;
55 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.RsaPrivateKeyFormat;
56 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.SubjectPublicKeyInfoFormat;
57 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.asymmetric.key.pair.grouping._private.key.type.CleartextPrivateKeyBuilder;
58 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.password.grouping.password.type.CleartextPasswordBuilder;
59 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
60 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
61 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
62 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev230417.inline.or.keystore.asymmetric.key.grouping.inline.or.keystore.InlineBuilder;
63 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev230417.inline.or.keystore.asymmetric.key.grouping.inline.or.keystore.inline.InlineDefinitionBuilder;
64 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev230417.netconf.client.initiate.stack.grouping.transport.tls.tls.TcpClientParametersBuilder;
65 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev230417.netconf.client.listen.stack.grouping.transport.ssh.ssh.SshClientParametersBuilder;
66 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev230417.netconf.client.listen.stack.grouping.transport.ssh.ssh.TcpServerParametersBuilder;
67 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ClientIdentityBuilder;
68 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.client.identity.PasswordBuilder;
69 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.SshServerGrouping;
70 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ClientAuthentication;
71 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ClientAuthenticationBuilder;
72 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ServerIdentity;
73 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ServerIdentityBuilder;
74 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.client.authentication.UsersBuilder;
75 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.client.authentication.users.UserBuilder;
76 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.server.identity.HostKeyBuilder;
77 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.server.identity.host.key.host.key.type.PublicKeyBuilder;
78 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev230417.TcpClientGrouping;
79 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev230417.TcpServerGrouping;
80 import org.opendaylight.yangtools.yang.common.Uint16;
81
82 @ExtendWith(MockitoExtension.class)
83 class NetconfClientFactoryImplTest {
84     private static final String USERNAME = "username";
85     private static final String PASSWORD = "pa$$w0rd";
86     private static final String RSA = "RSA";
87     private static final char[] EMPTY_SECRET = new char[0];
88
89     private static SSHTransportStackFactory serverTransportFactory;
90
91     @Mock
92     private NetconfClientSessionListener sessionListener;
93     @Mock
94     private TransportChannelListener serverTransportListener;
95     @Mock
96     private SshServerGrouping sshServerParams;
97
98     private NetconfClientFactory factory;
99     private TcpServerGrouping tcpServerParams;
100     private TcpClientGrouping tcpClientParams;
101
102
103     @BeforeAll
104     static void beforeAll() {
105         serverTransportFactory = new SSHTransportStackFactory("server", 0);
106     }
107
108     @AfterAll
109     static void afterAll() {
110         serverTransportFactory.close();
111     }
112
113     @BeforeEach
114     void beforeEach() throws Exception {
115         factory = new NetconfClientFactoryImpl();
116         doNothing().when(serverTransportListener).onTransportChannelEstablished(any());
117
118         // create temp socket to get available port for test
119         final var socket = new ServerSocket(0);
120         final var address = IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress());
121         final var port = new PortNumber(Uint16.valueOf(socket.getLocalPort()));
122         socket.close();
123
124         tcpServerParams = new TcpServerParametersBuilder().setLocalAddress(address).setLocalPort(port).build();
125         tcpClientParams =
126             new TcpClientParametersBuilder().setRemoteAddress(new Host(address)).setRemotePort(port).build();
127     }
128
129     @AfterEach
130     void afterEach() throws Exception {
131         if (factory != null) {
132             factory.close();
133         }
134     }
135
136     @Test
137     void tcpClient() throws Exception {
138         final var server = TCPServer.listen(serverTransportListener,
139             serverTransportFactory.newServerBootstrap(), tcpServerParams).get(1, TimeUnit.SECONDS);
140         try {
141             final var clientConfig = NetconfClientConfigurationBuilder.create()
142                 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TCP)
143                 .withTcpParameters(tcpClientParams).withSessionListener(sessionListener).build();
144             assertNotNull(factory.createClient(clientConfig));
145             verify(serverTransportListener, timeout(1000L))
146                 .onTransportChannelEstablished(any(TransportChannel.class));
147         } finally {
148             server.shutdown().get(1, TimeUnit.SECONDS);
149         }
150     }
151
152     @Test
153     void tlsClient() throws Exception {
154         final var keyStore = buildKeystoreWithGeneratedCertificate();
155         final var keyMgr = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
156         keyMgr.init(keyStore, EMPTY_SECRET);
157         final var trustMgr = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
158         trustMgr.init(keyStore);
159         final var serverContext = SslContextBuilder.forServer(keyMgr).trustManager(trustMgr).build();
160         final var clientContext = SslContextBuilder.forClient().keyManager(keyMgr).trustManager(trustMgr).build();
161
162         final var server = TLSServer.listen(serverTransportListener, serverTransportFactory.newServerBootstrap(),
163             tcpServerParams, channel -> serverContext.newHandler(channel.alloc())).get(1, TimeUnit.SECONDS);
164         try {
165             final var clientConfig = NetconfClientConfigurationBuilder.create()
166                 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TLS)
167                 .withTcpParameters(tcpClientParams)
168                 .withTransportSslHandlerFactory(channel -> clientContext.newHandler(channel.alloc()))
169                 .withSessionListener(sessionListener).build();
170             assertNotNull(factory.createClient(clientConfig));
171             verify(serverTransportListener, timeout(1000L))
172                 .onTransportChannelEstablished(any(TransportChannel.class));
173         } finally {
174             server.shutdown().get(1, TimeUnit.SECONDS);
175         }
176     }
177
178     private static KeyStore buildKeystoreWithGeneratedCertificate() throws Exception {
179         // key pair
180         final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
181         keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4), new SecureRandom());
182         final var keyPair = keyPairGenerator.generateKeyPair();
183         // certificate
184         final var now = Instant.now();
185         final var contentSigner = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
186         final var x500Name = new X500Name("CN=TestCertificate");
187         final var certificateBuilder = new JcaX509v3CertificateBuilder(x500Name,
188             BigInteger.valueOf(now.toEpochMilli()),
189             Date.from(now), Date.from(now.plus(Duration.ofDays(365))),
190             x500Name,
191             keyPair.getPublic());
192         final var certificate = new JcaX509CertificateConverter()
193             .setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner));
194         // keystore with certificate and key
195         final var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
196         keyStore.load(null, null);
197         keyStore.setCertificateEntry("cert", certificate);
198         keyStore.setKeyEntry("key", keyPair.getPrivate(), EMPTY_SECRET, new Certificate[]{certificate});
199         return keyStore;
200     }
201
202     @Test
203     void sshClient() throws Exception {
204         doReturn(buildSshServerIdentity()).when(sshServerParams).getServerIdentity();
205         doReturn(buildSshClientAuth()).when(sshServerParams).getClientAuthentication();
206         doReturn(null).when(sshServerParams).getTransportParams();
207         doReturn(null).when(sshServerParams).getKeepalives();
208
209         final var server = serverTransportFactory.listenServer("netconf", serverTransportListener, tcpServerParams,
210             sshServerParams).get(10, TimeUnit.SECONDS);
211
212         try {
213             final var clientConfig = NetconfClientConfigurationBuilder.create()
214                 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.SSH)
215                 .withTcpParameters(tcpClientParams)
216                 .withSshParameters(new SshClientParametersBuilder()
217                     .setClientIdentity(new ClientIdentityBuilder().setUsername(USERNAME)
218                         .setPassword(new PasswordBuilder().setPasswordType(
219                             new CleartextPasswordBuilder().setCleartextPassword(PASSWORD)
220                                 .build()).build()).build())
221                     .build())
222                 .withSessionListener(sessionListener)
223                 .withConnectionTimeoutMillis(10_000)
224                 .build();
225             assertNotNull(factory.createClient(clientConfig));
226             verify(serverTransportListener, timeout(10_000L))
227                 .onTransportChannelEstablished(any(TransportChannel.class));
228         } finally {
229             server.shutdown().get(1, TimeUnit.SECONDS);
230         }
231     }
232
233     private static ServerIdentity buildSshServerIdentity() throws Exception {
234         final var keyPair = KeyPairGenerator.getInstance(RSA).generateKeyPair();
235         final var inlineDef = new InlineDefinitionBuilder()
236             .setPublicKeyFormat(SubjectPublicKeyInfoFormat.VALUE)
237             .setPublicKey(keyPair.getPublic().getEncoded())
238             .setPrivateKeyFormat(RsaPrivateKeyFormat.VALUE)
239             .setPrivateKeyType(
240                 new CleartextPrivateKeyBuilder().setCleartextPrivateKey(
241                     keyPair.getPrivate().getEncoded()
242                 ).build()
243             ).build();
244         final var inline = new InlineBuilder().setInlineDefinition(inlineDef).build();
245         final var publicKey = new PublicKeyBuilder().setPublicKey(
246             new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417
247                 .ssh.server.grouping.server.identity.host.key.host.key.type._public.key
248                 .PublicKeyBuilder().setInlineOrKeystore(inline).build()
249         ).build();
250         return new ServerIdentityBuilder().setHostKey(
251             List.of(new HostKeyBuilder().setName("test-name").setHostKeyType(publicKey).build())
252         ).build();
253     }
254
255     private static ClientAuthentication buildSshClientAuth() {
256         final var user = new UserBuilder().setName(USERNAME).setPassword(new CryptHash("$0$" + PASSWORD)).build();
257         return new ClientAuthenticationBuilder().setUsers(
258             new UsersBuilder().setUser(Map.of(user.key(), user)).build()
259         ).build();
260     }
261 }