From 2811bdba8e85b55cd65cd8dc9547ec45d1e03a69 Mon Sep 17 00:00:00 2001 From: Ruslan Kashapov Date: Thu, 5 Oct 2023 12:40:02 +0300 Subject: [PATCH] External service integration support for TLS transport 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 Signed-off-by: Robert Varga --- .../transport/tls/SslHandlerFactory.java | 28 ++++++++++ .../netconf/transport/tls/TLSClient.java | 13 ++++- .../netconf/transport/tls/TLSServer.java | 15 +++++- .../transport/tls/TLSTransportStack.java | 14 ++--- .../netconf/transport/tls/TestUtils.java | 5 +- .../transport/tls/TlsClientServerTest.java | 53 ++++++++++++++++--- 6 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/SslHandlerFactory.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 index 0000000000..f216aca1f1 --- /dev/null +++ b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/SslHandlerFactory.java @@ -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); +} diff --git a/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSClient.java b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSClient.java index b3ec9b7591..facb096e4f 100644 --- a/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSClient.java +++ b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSClient.java @@ -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 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 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 listen(final TransportChannelListener listener, final ServerBootstrap bootstrap, final TcpServerGrouping listenParams, final TlsClientGrouping clientParams) throws UnsupportedConfigurationException { diff --git a/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSServer.java b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSServer.java index 3f0e52726b..d927a653ec 100644 --- a/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSServer.java +++ b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSServer.java @@ -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 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 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 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(); diff --git a/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSTransportStack.java b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSTransportStack.java index 2fd483a386..53565546d4 100644 --- a/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSTransportStack.java +++ b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/TLSTransportStack.java @@ -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 { diff --git a/transport/transport-tls/src/test/java/org/opendaylight/netconf/transport/tls/TestUtils.java b/transport/transport-tls/src/test/java/org/opendaylight/netconf/transport/tls/TestUtils.java index 3d666b1c54..436a2e732f 100644 --- a/transport/transport-tls/src/test/java/org/opendaylight/netconf/transport/tls/TestUtils.java +++ b/transport/transport-tls/src/test/java/org/opendaylight/netconf/transport/tls/TestUtils.java @@ -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) { } } diff --git a/transport/transport-tls/src/test/java/org/opendaylight/netconf/transport/tls/TlsClientServerTest.java b/transport/transport-tls/src/test/java/org/opendaylight/netconf/transport/tls/TlsClientServerTest.java index 622802eb6c..d04efaf6c2 100644 --- a/transport/transport-tls/src/test/java/org/opendaylight/netconf/transport/tls/TlsClientServerTest.java +++ b/transport/transport-tls/src/test/java/org/opendaylight/netconf/transport/tls/TlsClientServerTest.java @@ -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 serverFuture, + final ListenableFuture 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()); -- 2.36.6