2 * Copyright (c) 2023 PANTHEON.tech s.r.o. All rights reserved.
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
8 package org.opendaylight.netconf.transport.tls;
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;
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.List;
40 import java.util.concurrent.TimeUnit;
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.DisplayName;
45 import org.junit.jupiter.api.Test;
46 import org.junit.jupiter.api.extension.ExtendWith;
47 import org.junit.jupiter.params.ParameterizedTest;
48 import org.junit.jupiter.params.provider.ValueSource;
49 import org.mockito.ArgumentCaptor;
50 import org.mockito.Captor;
51 import org.mockito.Mock;
52 import org.mockito.junit.jupiter.MockitoExtension;
53 import org.opendaylight.netconf.transport.api.TransportChannel;
54 import org.opendaylight.netconf.transport.api.TransportChannelListener;
55 import org.opendaylight.netconf.transport.tcp.NettyTransportSupport;
56 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.EcPrivateKeyFormat;
57 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.RsaPrivateKeyFormat;
58 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.SubjectPublicKeyInfoFormat;
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.tcp.client.rev230417.TcpClientGrouping;
63 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev230417.TcpServerGrouping;
64 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev230417.TlsClientGrouping;
65 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev230417.tls.client.grouping.ClientIdentityBuilder;
66 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev230417.tls.client.grouping.ServerAuthenticationBuilder;
67 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev230417.TlsServerGrouping;
68 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev230417.tls.server.grouping.ClientAuthenticationBuilder;
69 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev230417.tls.server.grouping.ServerIdentityBuilder;
70 import org.opendaylight.yangtools.yang.common.Uint16;
72 @ExtendWith(MockitoExtension.class)
73 class TlsClientServerTest {
76 private TcpClientGrouping tcpClientConfig;
78 private TlsClientGrouping tlsClientConfig;
80 private TransportChannelListener clientListener;
82 private TcpServerGrouping tcpServerConfig;
84 private TlsServerGrouping tlsServerConfig;
86 private TransportChannelListener serverListener;
89 ArgumentCaptor<TransportChannel> clientTransportChannelCaptor;
91 ArgumentCaptor<TransportChannel> serverTransportChannelCaptor;
93 private static EventLoopGroup group;
94 private ServerSocket socket;
97 static void beforeAll() {
98 group = NettyTransportSupport.newEventLoopGroup("IntegrationTest");
102 static void afterAll() {
103 group.shutdownGracefully();
108 void beforeEach() throws IOException {
110 // create temp socket to get available port for test
111 socket = new ServerSocket(0);
112 final var localAddress = IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress());
113 final var localPort = new PortNumber(Uint16.valueOf(socket.getLocalPort()));
116 when(tcpServerConfig.getLocalAddress()).thenReturn(localAddress);
117 when(tcpServerConfig.requireLocalAddress()).thenCallRealMethod();
118 when(tcpServerConfig.getLocalPort()).thenReturn(localPort);
119 when(tcpServerConfig.requireLocalPort()).thenCallRealMethod();
121 when(tcpClientConfig.getRemoteAddress()).thenReturn(new Host(localAddress));
122 when(tcpClientConfig.requireRemoteAddress()).thenCallRealMethod();
123 when(tcpClientConfig.getRemotePort()).thenReturn(localPort);
124 when(tcpClientConfig.requireRemotePort()).thenCallRealMethod();
127 @ParameterizedTest(name = "TLS using X.509 certificates: {0}")
128 @ValueSource(strings = {RSA_ALGORITHM, EC_ALGORITHM})
129 void itWithCertificateConfig(final String algorithm) throws Exception {
131 final var data = generateX509CertData(algorithm);
133 // common config parts
134 var inlineOrKeystore = buildEndEntityCertWithKeyGrouping(
135 SubjectPublicKeyInfoFormat.VALUE, data.publicKey(),
136 isRSA(algorithm) ? RsaPrivateKeyFormat.VALUE : EcPrivateKeyFormat.VALUE,
137 data.privateKey(), data.certBytes()).getInlineOrKeystore();
138 var inlineOrTrustStore = buildInlineOrTruststore(Map.of("cert", data.certBytes()));
141 final var clientIdentity = new ClientIdentityBuilder()
142 .setAuthType(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev230417
143 .tls.client.grouping.client.identity.auth.type.CertificateBuilder()
144 .setCertificate(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev230417
145 .tls.client.grouping.client.identity.auth.type.certificate.CertificateBuilder()
146 .setInlineOrKeystore(inlineOrKeystore)
150 final var serverAuth = new ServerAuthenticationBuilder()
151 .setCaCerts(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev230417
152 .tls.client.grouping.server.authentication.CaCertsBuilder()
153 .setInlineOrTruststore(inlineOrTrustStore)
156 when(tlsClientConfig.getClientIdentity()).thenReturn(clientIdentity);
157 when(tlsClientConfig.getServerAuthentication()).thenReturn(serverAuth);
160 final var serverIdentity = new ServerIdentityBuilder()
161 .setAuthType(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev230417
162 .tls.server.grouping.server.identity.auth.type.CertificateBuilder()
163 .setCertificate(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev230417
164 .tls.server.grouping.server.identity.auth.type.certificate.CertificateBuilder()
165 .setInlineOrKeystore(inlineOrKeystore)
169 final var clientAuth = new ClientAuthenticationBuilder()
170 .setCaCerts(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev230417
171 .tls.server.grouping.client.authentication.CaCertsBuilder()
172 .setInlineOrTruststore(inlineOrTrustStore)
175 when(tlsServerConfig.getServerIdentity()).thenReturn(serverIdentity);
176 when(tlsServerConfig.getClientAuthentication()).thenReturn(clientAuth);
179 TLSServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group),
180 tcpServerConfig, tlsServerConfig),
181 TLSClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
182 tcpClientConfig, tlsClientConfig)
187 @DisplayName("External SslHandlerFactory integration")
188 void sslHandlerFactory() throws Exception {
190 final var serverKs = buildKeystoreWithGeneratedCert(RSA_ALGORITHM);
191 final var clientKs = buildKeystoreWithGeneratedCert(EC_ALGORITHM);
192 final var serverContext = SslContextBuilder.forServer(buildKeyManagerFactory(serverKs))
193 .clientAuth(ClientAuth.REQUIRE).trustManager(buildTrustManagerFactory(clientKs)).build();
194 final var clientContext = SslContextBuilder.forClient().keyManager(buildKeyManagerFactory(clientKs))
195 .trustManager(buildTrustManagerFactory(serverKs)).build();
198 TLSServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group),
199 tcpServerConfig, channel -> serverContext.newHandler(channel.alloc())),
200 TLSClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
201 tcpClientConfig, channel -> clientContext.newHandler(channel.alloc()))
205 private static KeyStore buildKeystoreWithGeneratedCert(final String algorithm) throws Exception {
206 final var data = generateX509CertData(algorithm);
207 final var ret = newKeyStore();
208 ret.setCertificateEntry("certificate", data.certificate());
209 ret.setKeyEntry("key", data.keyPair().getPrivate(), new char[0], new Certificate[]{data.certificate()});
213 private void integrationTest(final ListenableFuture<TLSServer> serverFuture,
214 final ListenableFuture<TLSClient> clientFuture) throws Exception {
216 final var server = serverFuture.get(2, TimeUnit.SECONDS);
218 // connect with client
219 final var client = clientFuture.get(2, TimeUnit.SECONDS);
221 verify(serverListener, timeout(500))
222 .onTransportChannelEstablished(serverTransportChannelCaptor.capture());
223 verify(clientListener, timeout(500))
224 .onTransportChannelEstablished(clientTransportChannelCaptor.capture());
225 // validate channels are in expected state
226 var serverChannel = assertChannel(serverTransportChannelCaptor.getAllValues());
227 var clientChannel = assertChannel(clientTransportChannelCaptor.getAllValues());
228 // validate channels are connecting same sockets
229 assertEquals(serverChannel.remoteAddress(), clientChannel.localAddress());
230 assertEquals(serverChannel.localAddress(), clientChannel.remoteAddress());
233 client.shutdown().get(2, TimeUnit.SECONDS);
236 server.shutdown().get(2, TimeUnit.SECONDS);
240 private static Channel assertChannel(final List<TransportChannel> transportChannels) {
241 assertNotNull(transportChannels);
242 assertEquals(1, transportChannels.size());
243 final var channel = assertInstanceOf(TLSTransportChannel.class, transportChannels.get(0)).channel();
244 assertNotNull(channel);
245 assertTrue(channel.isOpen()); // connection is open
246 assertNotNull(channel.pipeline().get(SslHandler.class)); // has an SSL handler within a pipeline