External service integration support for TLS transport 45/108245/22
authorRuslan Kashapov <ruslan.kashapov@pantheon.tech>
Thu, 5 Oct 2023 09:40:02 +0000 (12:40 +0300)
committerRobert Varga <robert.varga@pantheon.tech>
Sat, 14 Oct 2023 01:46:56 +0000 (03:46 +0200)
Netconf-topology uses own service to build SslHandler based on
certificates/keys data retrieved from datastore.

JIRA: NETCONF-1106
Change-Id: I7f60e7510852054b214c8aa0cc0198e9423d4954
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/SslHandlerFactory.java [new file with mode: 0644]
transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSClient.java
transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSServer.java
transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSTransportStack.java
transport/transport-tls/src/test/java/org/opendaylight/netconf/transport/tls/TestUtils.java
transport/transport-tls/src/test/java/org/opendaylight/netconf/transport/tls/TlsClientServerTest.java

diff --git a/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/SslHandlerFactory.java b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/SslHandlerFactory.java
new file mode 100644 (file)
index 0000000..f216aca
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tls;
+
+import com.google.common.annotations.Beta;
+import io.netty.channel.Channel;
+import io.netty.handler.ssl.SslHandler;
+
+/**
+ * Extension interface for external service integration with TLS transport. Used to build {@link TLSClient} and
+ * {@link TLSServer} instances.
+ */
+@Beta
+@FunctionalInterface
+public interface SslHandlerFactory {
+    /**
+     * Builds {@link SslHandler} instance for given {@link Channel}.
+     *
+     * @param channel channel
+     * @return A SslHandler
+     */
+    SslHandler createSslHandler(Channel channel);
+}
index b3ec9b7591a5b09ed95c144511cb6868081156f2..facb096e4f8dba2691df25b67c81a6da88f7422a 100644 (file)
@@ -32,13 +32,24 @@ public final class TLSClient extends TLSTransportStack {
         super(listener, sslContext);
     }
 
+    private TLSClient(final TransportChannelListener listener, final SslHandlerFactory factory) {
+        super(listener, factory);
+    }
+
     public static @NonNull ListenableFuture<TLSClient> connect(final TransportChannelListener listener,
             final Bootstrap bootstrap, final TcpClientGrouping connectParams, final TlsClientGrouping clientParams)
-            throws UnsupportedConfigurationException {
+                throws UnsupportedConfigurationException {
         final var client = newClient(listener, clientParams);
         return transformUnderlay(client, TCPClient.connect(client.asListener(), bootstrap, connectParams));
     }
 
+    public static @NonNull ListenableFuture<TLSClient> connect(final TransportChannelListener listener,
+            final Bootstrap bootstrap, final TcpClientGrouping connectParams, final SslHandlerFactory factory)
+                throws UnsupportedConfigurationException {
+        final var client = new TLSClient(listener, factory);
+        return transformUnderlay(client, TCPClient.connect(client.asListener(), bootstrap, connectParams));
+    }
+
     public static @NonNull ListenableFuture<TLSClient> listen(final TransportChannelListener listener,
             final ServerBootstrap bootstrap, final TcpServerGrouping listenParams, final TlsClientGrouping clientParams)
             throws UnsupportedConfigurationException {
index 3f0e52726bc08b8eafec672fedc461186c051c4b..d927a653ec82ca730418d7a15366e7e648c67197 100644 (file)
@@ -33,20 +33,31 @@ public final class TLSServer extends TLSTransportStack {
         super(listener, sslContext);
     }
 
+    private TLSServer(final TransportChannelListener listener, final SslHandlerFactory factory) {
+        super(listener, factory);
+    }
+
     public static @NonNull ListenableFuture<TLSServer> connect(final TransportChannelListener listener,
             final Bootstrap bootstrap, final TcpClientGrouping connectParams, final TlsServerGrouping serverParams)
-            throws UnsupportedConfigurationException {
+                throws UnsupportedConfigurationException {
         final var server = newServer(listener, serverParams);
         return transformUnderlay(server, TCPClient.connect(server.asListener(), bootstrap, connectParams));
     }
 
     public static @NonNull ListenableFuture<TLSServer> listen(final TransportChannelListener listener,
             final ServerBootstrap bootstrap, final TcpServerGrouping listenParams, final TlsServerGrouping serverParams)
-            throws UnsupportedConfigurationException {
+                throws UnsupportedConfigurationException {
         final var server = newServer(listener, serverParams);
         return transformUnderlay(server, TCPServer.listen(server.asListener(), bootstrap, listenParams));
     }
 
+    public static @NonNull ListenableFuture<TLSServer> listen(final TransportChannelListener listener,
+            final ServerBootstrap bootstrap, final TcpServerGrouping listenParams, final SslHandlerFactory factory)
+                throws UnsupportedConfigurationException {
+        final var server = new TLSServer(listener, factory);
+        return transformUnderlay(server, TCPServer.listen(server.asListener(), bootstrap, listenParams));
+    }
+
     private static TLSServer newServer(final TransportChannelListener listener, final TlsServerGrouping serverParams)
             throws UnsupportedConfigurationException {
         final var serverIdentity = serverParams.getServerIdentity();
index 2fd483a386bfa8897cd88d8c221f4d3d23d35b45..53565546d4d7055585ee15e81ba3099dcf093891 100644 (file)
@@ -97,17 +97,21 @@ public abstract sealed class TLSTransportStack extends AbstractOverlayTransportS
                     .put(TlsEcdheRsaWithChacha20Poly1305Sha256.VALUE, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256")
                     .build();
 
-    private volatile @NonNull SslContext sslContext;
+    private final SslHandlerFactory factory;
 
     TLSTransportStack(final TransportChannelListener listener, final SslContext sslContext) {
+        this(listener, channel -> sslContext.newHandler(channel.alloc()));
+    }
+
+    TLSTransportStack(final TransportChannelListener listener, final SslHandlerFactory factory) {
         super(listener);
-        this.sslContext = requireNonNull(sslContext);
+        this.factory = requireNonNull(factory);
     }
 
     @Override
     protected final void onUnderlayChannelEstablished(final TransportChannel underlayChannel) {
         final var channel = underlayChannel.channel();
-        final var sslHandler = sslContext.newHandler(channel.alloc());
+        final var sslHandler = factory.createSslHandler(channel);
 
         channel.pipeline().addLast(sslHandler);
         sslHandler.handshakeFuture().addListener(future -> {
@@ -121,10 +125,6 @@ public abstract sealed class TLSTransportStack extends AbstractOverlayTransportS
         });
     }
 
-    final void setSslContext(final SslContext sslContext) {
-        this.sslContext = requireNonNull(sslContext);
-    }
-
     static KeyManagerFactory newKeyManager(
             final @NonNull InlineOrKeystoreEndEntityCertWithKeyGrouping endEntityCert
     ) throws UnsupportedConfigurationException {
index 3d666b1c54a9fc5bbfe35100419e5fe2c09513a9..436a2e732f9309d6a92c11256ef64e07d6789910 100644 (file)
@@ -110,7 +110,7 @@ public final class TestUtils {
         final var certificate = generateCertificate(keyPair, isRSA(algorithm) ? "SHA256withRSA" : "SHA256withECDSA");
         final var publicKeyBytes = keyPair.getPublic().getEncoded();
         final var privateKeyBytes = keyPair.getPrivate().getEncoded();
-        return new X509CertData(certificate.getEncoded(), publicKeyBytes, privateKeyBytes,
+        return new X509CertData(certificate, keyPair, certificate.getEncoded(), publicKeyBytes, privateKeyBytes,
                 OpenSSHPublicKeyUtil.encodePublicKey(PublicKeyFactory.createKey(publicKeyBytes)));
     }
 
@@ -133,6 +133,7 @@ public final class TestUtils {
         return KeyUtils.RSA_ALGORITHM.equals(algorithm);
     }
 
-    public record X509CertData(byte[] certBytes, byte[] publicKey, byte[] privateKey, byte[] sshPublicKey) {
+    public record X509CertData(X509Certificate certificate, KeyPair keyPair, byte[] certBytes, byte[] publicKey,
+        byte[] privateKey, byte[] sshPublicKey) {
     }
 }
index 622802eb6c191fac63c58cee1213322c64913dfa..d04efaf6c22b1a4acdfa3d5fb2d278986719343c 100644 (file)
@@ -14,6 +14,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.opendaylight.netconf.transport.tls.KeyStoreUtils.buildKeyManagerFactory;
+import static org.opendaylight.netconf.transport.tls.KeyStoreUtils.buildTrustManagerFactory;
+import static org.opendaylight.netconf.transport.tls.KeyStoreUtils.newKeyStore;
 import static org.opendaylight.netconf.transport.tls.KeyUtils.EC_ALGORITHM;
 import static org.opendaylight.netconf.transport.tls.KeyUtils.RSA_ALGORITHM;
 import static org.opendaylight.netconf.transport.tls.TestUtils.buildEndEntityCertWithKeyGrouping;
@@ -21,18 +24,25 @@ import static org.opendaylight.netconf.transport.tls.TestUtils.buildInlineOrTrus
 import static org.opendaylight.netconf.transport.tls.TestUtils.generateX509CertData;
 import static org.opendaylight.netconf.transport.tls.TestUtils.isRSA;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import io.netty.channel.Channel;
 import io.netty.channel.EventLoopGroup;
+import io.netty.handler.ssl.ClientAuth;
+import io.netty.handler.ssl.SslContextBuilder;
 import io.netty.handler.ssl.SslHandler;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.ServerSocket;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
@@ -165,17 +175,48 @@ class TlsClientServerTest {
         when(tlsServerConfig.getServerIdentity()).thenReturn(serverIdentity);
         when(tlsServerConfig.getClientAuthentication()).thenReturn(clientAuth);
 
-        integrationTest();
+        integrationTest(
+            TLSServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group),
+                tcpServerConfig, tlsServerConfig),
+            TLSClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
+                tcpClientConfig, tlsClientConfig)
+        );
     }
 
-    private void integrationTest() throws Exception {
+    @Test
+    @DisplayName("External SslHandlerFactory integration")
+    void sslHandlerFactory() throws Exception {
+
+        final var serverKs = buildKeystoreWithGeneratedCert(RSA_ALGORITHM);
+        final var clientKs = buildKeystoreWithGeneratedCert(EC_ALGORITHM);
+        final var serverContext = SslContextBuilder.forServer(buildKeyManagerFactory(serverKs))
+            .clientAuth(ClientAuth.REQUIRE).trustManager(buildTrustManagerFactory(clientKs)).build();
+        final var clientContext = SslContextBuilder.forClient().keyManager(buildKeyManagerFactory(clientKs))
+            .trustManager(buildTrustManagerFactory(serverKs)).build();
+
+        integrationTest(
+            TLSServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group),
+                tcpServerConfig, channel -> serverContext.newHandler(channel.alloc())),
+            TLSClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
+                tcpClientConfig, channel -> clientContext.newHandler(channel.alloc()))
+        );
+    }
+
+    private static KeyStore buildKeystoreWithGeneratedCert(final String algorithm) throws Exception {
+        final var data = generateX509CertData(algorithm);
+        final var ret = newKeyStore();
+        ret.setCertificateEntry("certificate", data.certificate());
+        ret.setKeyEntry("key", data.keyPair().getPrivate(), new char[0], new Certificate[]{data.certificate()});
+        return ret;
+    }
+
+    private void integrationTest(final ListenableFuture<TLSServer> serverFuture,
+            final ListenableFuture<TLSClient> clientFuture) throws Exception {
         // start server
-        final var server = TLSServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group),
-                tcpServerConfig, tlsServerConfig).get(2, TimeUnit.SECONDS);
+        final var server = serverFuture.get(2, TimeUnit.SECONDS);
         try {
             // connect with client
-            final var client = TLSClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
-                    tcpClientConfig, tlsClientConfig).get(2, TimeUnit.SECONDS);
+            final var client = clientFuture.get(2, TimeUnit.SECONDS);
             try {
                 verify(serverListener, timeout(500))
                         .onTransportChannelEstablished(serverTransportChannelCaptor.capture());