45f75378d29afcbf83925b0f90d424cad357269d
[netconf.git] / transport / transport-tls / src / test / java / org / opendaylight / netconf / transport / tls / TlsClientServerTest.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.tls;
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.tls.KeyStoreUtils.buildKeyManagerFactory;
18 import static org.opendaylight.netconf.transport.tls.KeyStoreUtils.buildTrustManagerFactory;
19 import static org.opendaylight.netconf.transport.tls.KeyStoreUtils.newKeyStore;
20 import static org.opendaylight.netconf.transport.tls.KeyUtils.EC_ALGORITHM;
21 import static org.opendaylight.netconf.transport.tls.KeyUtils.RSA_ALGORITHM;
22 import static org.opendaylight.netconf.transport.tls.TestUtils.buildEndEntityCertWithKeyGrouping;
23 import static org.opendaylight.netconf.transport.tls.TestUtils.buildInlineOrTruststore;
24 import static org.opendaylight.netconf.transport.tls.TestUtils.generateX509CertData;
25 import static org.opendaylight.netconf.transport.tls.TestUtils.isRSA;
26
27 import com.google.common.util.concurrent.ListenableFuture;
28 import io.netty.channel.Channel;
29 import io.netty.channel.EventLoopGroup;
30 import io.netty.handler.ssl.ClientAuth;
31 import io.netty.handler.ssl.SslContextBuilder;
32 import io.netty.handler.ssl.SslHandler;
33 import java.io.IOException;
34 import java.net.InetAddress;
35 import java.net.ServerSocket;
36 import java.security.KeyStore;
37 import java.security.cert.Certificate;
38 import java.util.ArrayList;
39 import java.util.Comparator;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.concurrent.TimeUnit;
43 import org.junit.jupiter.api.AfterAll;
44 import org.junit.jupiter.api.BeforeAll;
45 import org.junit.jupiter.api.BeforeEach;
46 import org.junit.jupiter.api.DisplayName;
47 import org.junit.jupiter.api.Test;
48 import org.junit.jupiter.api.extension.ExtendWith;
49 import org.junit.jupiter.params.ParameterizedTest;
50 import org.junit.jupiter.params.provider.ValueSource;
51 import org.mockito.ArgumentCaptor;
52 import org.mockito.Captor;
53 import org.mockito.Mock;
54 import org.mockito.junit.jupiter.MockitoExtension;
55 import org.opendaylight.netconf.transport.api.TransportChannel;
56 import org.opendaylight.netconf.transport.api.TransportChannelListener;
57 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
58 import org.opendaylight.netconf.transport.tcp.NettyTransportSupport;
59 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.EcPrivateKeyFormat;
60 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.RsaPrivateKeyFormat;
61 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.SubjectPublicKeyInfoFormat;
62 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
63 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
64 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
65 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev231228.TcpClientGrouping;
66 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev231228.TcpServerGrouping;
67 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.TlsClientGrouping;
68 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.tls.client.grouping.ClientIdentityBuilder;
69 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.tls.client.grouping.ServerAuthenticationBuilder;
70 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.TlsServerGrouping;
71 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.tls.server.grouping.ClientAuthenticationBuilder;
72 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.tls.server.grouping.ServerIdentityBuilder;
73 import org.opendaylight.yangtools.yang.common.Uint16;
74
75 @ExtendWith(MockitoExtension.class)
76 class TlsClientServerTest {
77
78     @Mock
79     private TcpClientGrouping tcpClientConfig;
80     @Mock
81     private TlsClientGrouping tlsClientConfig;
82     @Mock
83     private TransportChannelListener clientListener;
84     @Mock
85     private TcpServerGrouping tcpServerConfig;
86     @Mock
87     private TlsServerGrouping tlsServerConfig;
88     @Mock
89     private TransportChannelListener serverListener;
90     @Mock
91     private TransportChannelListener otherServerListener;
92
93     @Captor
94     ArgumentCaptor<TransportChannel> clientTransportChannelCaptor;
95     @Captor
96     ArgumentCaptor<TransportChannel> serverTransportChannelCaptor;
97
98     private static EventLoopGroup group;
99     private ServerSocket socket;
100
101     @BeforeAll
102     static void beforeAll() {
103         group = NettyTransportSupport.newEventLoopGroup("IntegrationTest");
104     }
105
106     @AfterAll
107     static void afterAll() {
108         group.shutdownGracefully();
109         group = null;
110     }
111
112     @BeforeEach
113     void beforeEach() throws IOException {
114
115         // create temp socket to get available port for test
116         socket = new ServerSocket(0);
117         final var localAddress = IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress());
118         final var localPort = new PortNumber(Uint16.valueOf(socket.getLocalPort()));
119         socket.close();
120
121         when(tcpServerConfig.getLocalAddress()).thenReturn(localAddress);
122         when(tcpServerConfig.requireLocalAddress()).thenCallRealMethod();
123         when(tcpServerConfig.getLocalPort()).thenReturn(localPort);
124         when(tcpServerConfig.requireLocalPort()).thenCallRealMethod();
125
126         when(tcpClientConfig.getRemoteAddress()).thenReturn(new Host(localAddress));
127         when(tcpClientConfig.requireRemoteAddress()).thenCallRealMethod();
128         when(tcpClientConfig.getRemotePort()).thenReturn(localPort);
129         when(tcpClientConfig.requireRemotePort()).thenCallRealMethod();
130     }
131
132     @ParameterizedTest(name = "TLS using X.509 certificates: {0}")
133     @ValueSource(strings = {RSA_ALGORITHM, EC_ALGORITHM})
134     void itWithCertificateConfig(final String algorithm) throws Exception {
135
136         final var data = generateX509CertData(algorithm);
137
138         // common config parts
139         var inlineOrKeystore = buildEndEntityCertWithKeyGrouping(
140                 SubjectPublicKeyInfoFormat.VALUE, data.publicKey(),
141                 isRSA(algorithm) ? RsaPrivateKeyFormat.VALUE : EcPrivateKeyFormat.VALUE,
142                 data.privateKey(), data.certBytes()).getInlineOrKeystore();
143         var inlineOrTrustStore = buildInlineOrTruststore(Map.of("cert", data.certBytes()));
144
145         // client config
146         final var clientIdentity = new ClientIdentityBuilder()
147             .setAuthType(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228
148                 .tls.client.grouping.client.identity.auth.type.CertificateBuilder()
149                 .setCertificate(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228
150                     .tls.client.grouping.client.identity.auth.type.certificate.CertificateBuilder()
151                     .setInlineOrKeystore(inlineOrKeystore)
152                     .build())
153                 .build())
154             .build();
155         final var serverAuth = new ServerAuthenticationBuilder()
156             .setCaCerts(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228
157                 .tls.client.grouping.server.authentication.CaCertsBuilder()
158                 .setInlineOrTruststore(inlineOrTrustStore)
159                 .build())
160             .build();
161         when(tlsClientConfig.getClientIdentity()).thenReturn(clientIdentity);
162         when(tlsClientConfig.getServerAuthentication()).thenReturn(serverAuth);
163
164         // server config
165         final var serverIdentity = new ServerIdentityBuilder()
166             .setAuthType(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228
167                 .tls.server.grouping.server.identity.auth.type.CertificateBuilder()
168                 .setCertificate(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228
169                     .tls.server.grouping.server.identity.auth.type.certificate.CertificateBuilder()
170                     .setInlineOrKeystore(inlineOrKeystore)
171                     .build())
172                 .build())
173             .build();
174         final var clientAuth = new ClientAuthenticationBuilder()
175             .setCaCerts(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228
176                 .tls.server.grouping.client.authentication.CaCertsBuilder()
177                 .setInlineOrTruststore(inlineOrTrustStore)
178                 .build())
179             .build();
180         when(tlsServerConfig.getServerIdentity()).thenReturn(serverIdentity);
181         when(tlsServerConfig.getClientAuthentication()).thenReturn(clientAuth);
182
183         integrationTest(
184             () -> TLSServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group),
185                 tcpServerConfig, tlsServerConfig),
186             () -> TLSClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
187                 tcpClientConfig, tlsClientConfig)
188         );
189     }
190
191     @Test
192     @DisplayName("External SslHandlerFactory integration")
193     void sslHandlerFactory() throws Exception {
194
195         final var serverKs = buildKeystoreWithGeneratedCert(RSA_ALGORITHM);
196         final var clientKs = buildKeystoreWithGeneratedCert(EC_ALGORITHM);
197         final var serverContext = SslContextBuilder.forServer(buildKeyManagerFactory(serverKs))
198             .clientAuth(ClientAuth.REQUIRE).trustManager(buildTrustManagerFactory(clientKs)).build();
199         final var clientContext = SslContextBuilder.forClient().keyManager(buildKeyManagerFactory(clientKs))
200             .trustManager(buildTrustManagerFactory(serverKs)).build();
201
202         integrationTest(
203             () -> TLSServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group),
204                 tcpServerConfig, channel -> serverContext.newHandler(channel.alloc())),
205             () -> TLSClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
206                 tcpClientConfig, channel -> clientContext.newHandler(channel.alloc()))
207         );
208     }
209
210     private static KeyStore buildKeystoreWithGeneratedCert(final String algorithm) throws Exception {
211         final var data = generateX509CertData(algorithm);
212         final var ret = newKeyStore();
213         ret.setCertificateEntry("certificate", data.certificate());
214         ret.setKeyEntry("key", data.keyPair().getPrivate(), new char[0], new Certificate[]{data.certificate()});
215         return ret;
216     }
217
218     private void integrationTest(final Builder<TLSServer> serverBuilder,
219             final Builder<TLSClient> clientBuilder) throws Exception {
220         // start server
221         final var server = serverBuilder.build().get(2, TimeUnit.SECONDS);
222         try {
223             // connect with client
224             final var client = clientBuilder.build().get(2, TimeUnit.SECONDS);
225             try {
226                 verify(serverListener, timeout(500))
227                         .onTransportChannelEstablished(serverTransportChannelCaptor.capture());
228                 verify(clientListener, timeout(500))
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
237             } finally {
238                 client.shutdown().get(2, TimeUnit.SECONDS);
239             }
240         } finally {
241             server.shutdown().get(2, TimeUnit.SECONDS);
242         }
243     }
244
245     @Test
246     @DisplayName("Call-Home client + 2 servers with external SslHandlerFactory integration")
247     void callHome() throws Exception {
248
249         final var serverKs = buildKeystoreWithGeneratedCert(RSA_ALGORITHM);
250         final var clientKs = buildKeystoreWithGeneratedCert(EC_ALGORITHM);
251         final var serverContext = SslContextBuilder.forServer(buildKeyManagerFactory(serverKs))
252             .clientAuth(ClientAuth.REQUIRE).trustManager(buildTrustManagerFactory(clientKs)).build();
253         final var clientContext = SslContextBuilder.forClient().keyManager(buildKeyManagerFactory(clientKs))
254             .trustManager(buildTrustManagerFactory(serverKs)).build();
255
256         // start call-home client
257         final var client = TLSClient.listen(clientListener, NettyTransportSupport.newServerBootstrap().group(group),
258             tcpServerConfig, channel -> clientContext.newHandler(channel.alloc())).get(2, TimeUnit.SECONDS);
259         try {
260             // connect with call-home servers
261             final var server1 = TLSServer.connect(serverListener, NettyTransportSupport.newBootstrap().group(group),
262                 tcpClientConfig, channel -> serverContext.newHandler(channel.alloc())).get(2, TimeUnit.SECONDS);
263             final var server2 = TLSServer.connect(otherServerListener,
264                 NettyTransportSupport.newBootstrap().group(group),
265                 tcpClientConfig, channel -> serverContext.newHandler(channel.alloc())).get(2, TimeUnit.SECONDS);
266             try {
267                 verify(serverListener, timeout(500))
268                     .onTransportChannelEstablished(serverTransportChannelCaptor.capture());
269                 verify(otherServerListener, timeout(500))
270                     .onTransportChannelEstablished(serverTransportChannelCaptor.capture());
271                 verify(clientListener, timeout(500).times(2))
272                     .onTransportChannelEstablished(clientTransportChannelCaptor.capture());
273                 // extract channels sorted by server address
274                 var serverChannels = assertChannels(serverTransportChannelCaptor.getAllValues(), 2,
275                     Comparator.comparing((Channel channel) -> channel.localAddress().toString()));
276                 var clientChannels = assertChannels(clientTransportChannelCaptor.getAllValues(), 2,
277                     Comparator.comparing((Channel channel) -> channel.remoteAddress().toString()));
278                 for (int i = 0; i < 2; i++) {
279                     // validate channels are connecting same sockets
280                     assertEquals(serverChannels.get(i).remoteAddress(), clientChannels.get(i).localAddress());
281                     assertEquals(serverChannels.get(i).localAddress(), clientChannels.get(i).remoteAddress());
282                 }
283
284             } finally {
285                 server1.shutdown().get(2, TimeUnit.SECONDS);
286                 server2.shutdown().get(2, TimeUnit.SECONDS);
287             }
288         } finally {
289             client.shutdown().get(2, TimeUnit.SECONDS);
290         }
291     }
292
293     private static Channel assertChannel(final List<TransportChannel> transportChannels) {
294         assertNotNull(transportChannels);
295         assertEquals(1, transportChannels.size());
296         final var channel = assertInstanceOf(TLSTransportChannel.class, transportChannels.get(0)).channel();
297         assertNotNull(channel);
298         assertTrue(channel.isOpen()); // connection is open
299         assertNotNull(channel.pipeline().get(SslHandler.class)); //  has an SSL handler within a pipeline
300         return channel;
301     }
302
303     private static List<Channel> assertChannels(final List<TransportChannel> transportChannels, final int channelsNum,
304         final Comparator<Channel> comparator) {
305         assertNotNull(transportChannels);
306         assertEquals(channelsNum, transportChannels.size());
307         final var res = new ArrayList<Channel>(channelsNum);
308         for (var transportChannel : transportChannels) {
309             final var channel = assertInstanceOf(TLSTransportChannel.class, transportChannel).channel();
310             assertNotNull(channel);
311             assertTrue(channel.isOpen());
312             assertNotNull(channel.pipeline().get(SslHandler.class));
313             res.add(channel);
314         }
315         res.sort(comparator);
316         return res;
317     }
318
319     private interface Builder<T extends TLSTransportStack> {
320         ListenableFuture<T> build() throws UnsupportedConfigurationException;
321     }
322 }