From: Robert Varga Date: Sun, 28 Jan 2024 17:40:33 +0000 (+0100) Subject: Rework SslHandlerFactory X-Git-Tag: v7.0.0~59 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=ac384e8c3d554eec7db593c519ae8a0653a2ce79;p=netconf.git Rework SslHandlerFactory Tracking down all the possible implementations and bridging of SslHandlerFactor is not nice. Turn SslHandlerFactory into an abstract class, with an explicit fixed specialization. This also allows us to put the configuration parsing bits to where users can find them -- and create dynamic SslContexts as appropriate. Furthermore this ends up differentiating the deprecated netconf.client.SslHandlerFactory -- which becomes SslContextFactory and we end up ditching the manual SSLEngine creation. JIRA: NETCONF-1237 Change-Id: I2cd4721336f9867921123521d7b18d91a8743835 Signed-off-by: Robert Varga --- diff --git a/apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/tls/CallHomeMountTlsAuthProvider.java b/apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/tls/CallHomeMountTlsAuthProvider.java index 4d96123f5f..0eb6c9d854 100644 --- a/apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/tls/CallHomeMountTlsAuthProvider.java +++ b/apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/tls/CallHomeMountTlsAuthProvider.java @@ -8,8 +8,8 @@ package org.opendaylight.netconf.callhome.mount.tls; import com.google.common.collect.ImmutableMultimap; -import io.netty.channel.Channel; -import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslContext; +import java.net.SocketAddress; import java.security.PublicKey; import java.util.List; import java.util.Map; @@ -25,8 +25,8 @@ import org.opendaylight.mdsal.binding.api.DataTreeIdentifier; import org.opendaylight.mdsal.binding.api.DataTreeModification; import org.opendaylight.mdsal.common.api.LogicalDatastoreType; import org.opendaylight.netconf.callhome.server.tls.CallHomeTlsAuthProvider; -import org.opendaylight.netconf.client.SslHandlerFactory; -import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider; +import org.opendaylight.netconf.client.SslContextFactory; +import org.opendaylight.netconf.client.mdsal.api.SslContextFactoryProvider; import org.opendaylight.netconf.keystore.legacy.NetconfKeystore; import org.opendaylight.netconf.keystore.legacy.NetconfKeystoreService; import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.NetconfCallhomeServer; @@ -45,22 +45,22 @@ import org.slf4j.LoggerFactory; @Component(service = CallHomeTlsAuthProvider.class, immediate = true) @Singleton -public final class CallHomeMountTlsAuthProvider implements CallHomeTlsAuthProvider, AutoCloseable { +public final class CallHomeMountTlsAuthProvider extends CallHomeTlsAuthProvider implements AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(CallHomeMountTlsAuthProvider.class); private final ConcurrentMap deviceToPrivateKey = new ConcurrentHashMap<>(); private final ConcurrentMap deviceToCertificate = new ConcurrentHashMap<>(); private final Registration allowedDevicesReg; private final Registration certificatesReg; - private final SslHandlerFactory sslHandlerFactory; + private final SslContextFactory sslContextFactory; private volatile ImmutableMultimap publicKeyToName = ImmutableMultimap.of(); @Inject @Activate - public CallHomeMountTlsAuthProvider(final @Reference SslHandlerFactoryProvider sslHandlerFactoryProvider, - final @Reference DataBroker dataBroker, final @Reference NetconfKeystoreService keystoreService) { - sslHandlerFactory = sslHandlerFactoryProvider.getSslHandlerFactory(null); + public CallHomeMountTlsAuthProvider(@Reference final SslContextFactoryProvider sslContextFactoryProvider, + @Reference final DataBroker dataBroker, @Reference final NetconfKeystoreService keystoreService) { + sslContextFactory = sslContextFactoryProvider.getSslContextFactory(null); allowedDevicesReg = dataBroker.registerTreeChangeListener( DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class)), @@ -108,8 +108,8 @@ public final class CallHomeMountTlsAuthProvider implements CallHomeTlsAuthProvid } @Override - public SslHandler createSslHandler(final Channel channel) { - return sslHandlerFactory.createSslHandler(Set.copyOf(deviceToPrivateKey.values())); + protected SslContext getSslContext(final SocketAddress remoteAddress) { + return sslContextFactory.createSslContext(Set.copyOf(deviceToPrivateKey.values())); } private final class AllowedDevicesMonitor implements DataTreeChangeListener { diff --git a/apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/server/tls/CallHomeTlsAuthProvider.java b/apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/server/tls/CallHomeTlsAuthProvider.java index ebc3ed71a9..a161b18349 100644 --- a/apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/server/tls/CallHomeTlsAuthProvider.java +++ b/apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/server/tls/CallHomeTlsAuthProvider.java @@ -15,7 +15,7 @@ import org.opendaylight.netconf.transport.tls.SslHandlerFactory; /** * Authentication provider. */ -public interface CallHomeTlsAuthProvider extends SslHandlerFactory { +public abstract class CallHomeTlsAuthProvider extends SslHandlerFactory { - @Nullable String idFor(@NonNull PublicKey publicKey); + public abstract @Nullable String idFor(@NonNull PublicKey publicKey); } diff --git a/apps/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/server/tls/CallHomeTlsServerTest.java b/apps/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/server/tls/CallHomeTlsServerTest.java index 4bce3578d0..54a7303e3c 100644 --- a/apps/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/server/tls/CallHomeTlsServerTest.java +++ b/apps/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/server/tls/CallHomeTlsServerTest.java @@ -18,13 +18,14 @@ import static org.mockito.Mockito.verify; import com.google.common.util.concurrent.SettableFuture; import io.netty.channel.Channel; import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.SslHandler; import io.netty.util.concurrent.Promise; import java.math.BigInteger; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; +import java.net.SocketAddress; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; @@ -47,7 +48,6 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -68,6 +68,7 @@ import org.opendaylight.netconf.server.osgi.AggregatedNetconfOperationServiceFac import org.opendaylight.netconf.transport.api.TransportChannel; import org.opendaylight.netconf.transport.api.TransportChannelListener; import org.opendaylight.netconf.transport.tcp.BootstrapFactory; +import org.opendaylight.netconf.transport.tls.FixedSslHandlerFactory; import org.opendaylight.netconf.transport.tls.TLSServer; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil; @@ -122,15 +123,14 @@ public class CallHomeTlsServerTest { // Auth provider final var authProvider = new CallHomeTlsAuthProvider() { @Override - public @Nullable String idFor(@NonNull - final PublicKey publicKey) { + public String idFor(final PublicKey publicKey) { // identify client 3 only return clientCert3.keyPair.getPublic().equals(publicKey) ? "client-id" : null; } @Override - public SslHandler createSslHandler(final Channel channel) { - return serverCtx.newHandler(channel.alloc()); + protected SslContext getSslContext(final SocketAddress remoteAddress) { + return serverCtx; } }; @@ -182,21 +182,21 @@ public class CallHomeTlsServerTest { // client 1 rejected on handshake, ensure exception client1 = TLSServer.connect( netconfTransportListener, bootstrapFactory.newBootstrap(), tcpConnectParams, - channel -> clientCtx1.newHandler(channel.alloc())).get(TIMEOUT, TimeUnit.MILLISECONDS); + new FixedSslHandlerFactory(clientCtx1)).get(TIMEOUT, TimeUnit.MILLISECONDS); verify(statusRecorder, timeout(TIMEOUT).times(1)) .onTransportChannelFailure(any(SSLHandshakeException.class)); // client 2 rejected because it's not identified by public key accepted on handshake stage client2 = TLSServer.connect( netconfTransportListener, bootstrapFactory.newBootstrap(), tcpConnectParams, - channel -> clientCtx2.newHandler(channel.alloc())).get(TIMEOUT, TimeUnit.MILLISECONDS); + new FixedSslHandlerFactory(clientCtx2)).get(TIMEOUT, TimeUnit.MILLISECONDS); verify(statusRecorder, timeout(TIMEOUT).times(1)) .reportUnknown(any(InetSocketAddress.class), eq(clientCert2.keyPair.getPublic())); // client 3 accepted client3 = TLSServer.connect( netconfTransportListener, bootstrapFactory.newBootstrap(), tcpConnectParams, - channel -> clientCtx3.newHandler(channel.alloc())).get(TIMEOUT, TimeUnit.MILLISECONDS); + new FixedSslHandlerFactory(clientCtx3)).get(TIMEOUT, TimeUnit.MILLISECONDS); // verify netconf session established verify(clientSessionListener, timeout(TIMEOUT).times(1)).onSessionUp(any(NetconfClientSession.class)); verify(serverSessionListener, timeout(TIMEOUT).times(1)).onSessionUp(any(NetconfServerSession.class)); diff --git a/apps/netconf-topology-singleton/src/test/java/org/opendaylight/netconf/topology/singleton/impl/MountPointEndToEndTest.java b/apps/netconf-topology-singleton/src/test/java/org/opendaylight/netconf/topology/singleton/impl/MountPointEndToEndTest.java index 48eba6c85f..fcadab47e1 100644 --- a/apps/netconf-topology-singleton/src/test/java/org/opendaylight/netconf/topology/singleton/impl/MountPointEndToEndTest.java +++ b/apps/netconf-topology-singleton/src/test/java/org/opendaylight/netconf/topology/singleton/impl/MountPointEndToEndTest.java @@ -93,7 +93,7 @@ import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences; import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceServices; import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceServices.Rpcs; import org.opendaylight.netconf.client.mdsal.api.SchemaResourceManager; -import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider; +import org.opendaylight.netconf.client.mdsal.api.SslContextFactoryProvider; import org.opendaylight.netconf.client.mdsal.impl.DefaultSchemaResourceManager; import org.opendaylight.netconf.common.NetconfTimer; import org.opendaylight.netconf.topology.singleton.impl.utils.ClusteringRpcException; @@ -193,7 +193,7 @@ public class MountPointEndToEndTest extends AbstractBaseSchemasTest { @Mock private CredentialProvider credentialProvider; @Mock - private SslHandlerFactoryProvider sslHandlerFactoryProvider; + private SslContextFactoryProvider sslHandlerFactoryProvider; @Mock private DOMMountPointListener masterMountPointListener; private final DOMMountPointService masterMountPointService = new DOMMountPointServiceImpl(); diff --git a/apps/netconf-topology/src/main/java/org/opendaylight/netconf/topology/spi/NetconfClientConfigurationBuilderFactoryImpl.java b/apps/netconf-topology/src/main/java/org/opendaylight/netconf/topology/spi/NetconfClientConfigurationBuilderFactoryImpl.java index d9562698c3..71fe16f681 100644 --- a/apps/netconf-topology/src/main/java/org/opendaylight/netconf/topology/spi/NetconfClientConfigurationBuilderFactoryImpl.java +++ b/apps/netconf-topology/src/main/java/org/opendaylight/netconf/topology/spi/NetconfClientConfigurationBuilderFactoryImpl.java @@ -18,9 +18,10 @@ import org.opendaylight.aaa.encrypt.AAAEncryptionService; import org.opendaylight.netconf.client.conf.NetconfClientConfiguration.NetconfClientProtocol; import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder; import org.opendaylight.netconf.client.mdsal.api.CredentialProvider; -import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider; +import org.opendaylight.netconf.client.mdsal.api.SslContextFactoryProvider; import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKeyFactory; import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyIdentityProvider; +import org.opendaylight.netconf.transport.tls.FixedSslHandlerFactory; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.password.grouping.password.type.CleartextPasswordBuilder; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev231228.netconf.client.initiate.stack.grouping.transport.ssh.ssh.SshClientParametersBuilder; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev231228.netconf.client.initiate.stack.grouping.transport.ssh.ssh.TcpClientParametersBuilder; @@ -44,7 +45,7 @@ import org.osgi.service.component.annotations.Reference; @Component @Singleton public final class NetconfClientConfigurationBuilderFactoryImpl implements NetconfClientConfigurationBuilderFactory { - private final SslHandlerFactoryProvider sslHandlerFactoryProvider; + private final SslContextFactoryProvider sslContextFactoryProvider; private final AAAEncryptionService encryptionService; private final CredentialProvider credentialProvider; @@ -53,10 +54,10 @@ public final class NetconfClientConfigurationBuilderFactoryImpl implements Netco public NetconfClientConfigurationBuilderFactoryImpl( @Reference final AAAEncryptionService encryptionService, @Reference final CredentialProvider credentialProvider, - @Reference final SslHandlerFactoryProvider sslHandlerFactoryProvider) { + @Reference final SslContextFactoryProvider sslHandlerContextProvider) { this.encryptionService = requireNonNull(encryptionService); this.credentialProvider = requireNonNull(credentialProvider); - this.sslHandlerFactoryProvider = requireNonNull(sslHandlerFactoryProvider); + sslContextFactoryProvider = requireNonNull(sslHandlerContextProvider); } @Override @@ -70,9 +71,10 @@ public final class NetconfClientConfigurationBuilderFactoryImpl implements Netco builder.withProtocol(NetconfClientProtocol.SSH); setSshParametersFromCredentials(builder, node.getCredentials()); } else if (protocol.getName() == Name.TLS) { - final var handlerFactory = sslHandlerFactoryProvider.getSslHandlerFactory(protocol.getSpecification()); + final var handlerFactory = sslContextFactoryProvider.getSslContextFactory(protocol.getSpecification()); + final var sslContext = handlerFactory.createSslContext(); builder.withProtocol(NetconfClientProtocol.TLS) - .withSslHandlerFactory(channel -> handlerFactory.createSslHandler()); + .withSslHandlerFactory(new FixedSslHandlerFactory(sslContext)); } else { throw new IllegalArgumentException("Unsupported protocol type: " + protocol.getName()); } diff --git a/apps/netconf-topology/src/test/java/org/opendaylight/netconf/topology/spi/NetconfClientConfigurationBuilderFactoryImplTest.java b/apps/netconf-topology/src/test/java/org/opendaylight/netconf/topology/spi/NetconfClientConfigurationBuilderFactoryImplTest.java index 8365b4594d..0447b63bd5 100644 --- a/apps/netconf-topology/src/test/java/org/opendaylight/netconf/topology/spi/NetconfClientConfigurationBuilderFactoryImplTest.java +++ b/apps/netconf-topology/src/test/java/org/opendaylight/netconf/topology/spi/NetconfClientConfigurationBuilderFactoryImplTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import io.netty.handler.ssl.SslContext; import java.util.NoSuchElementException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,11 +23,11 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.opendaylight.aaa.encrypt.AAAEncryptionService; import org.opendaylight.netconf.client.NetconfClientSessionListener; -import org.opendaylight.netconf.client.SslHandlerFactory; +import org.opendaylight.netconf.client.SslContextFactory; import org.opendaylight.netconf.client.conf.NetconfClientConfiguration; import org.opendaylight.netconf.client.conf.NetconfClientConfiguration.NetconfClientProtocol; import org.opendaylight.netconf.client.mdsal.api.CredentialProvider; -import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider; +import org.opendaylight.netconf.client.mdsal.api.SslContextFactoryProvider; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Ipv4Address; @@ -55,9 +56,11 @@ class NetconfClientConfigurationBuilderFactoryImplTest { @Mock private CredentialProvider credentialProvider; @Mock - private SslHandlerFactoryProvider sslHandlerFactoryProvider; + private SslContextFactoryProvider sslContextFactoryProvider; @Mock - private SslHandlerFactory sslHandlerFactory; + private SslContextFactory sslContextFactory; + @Mock + private SslContext sslContext; private NetconfNodeBuilder nodeBuilder; private NetconfClientConfigurationBuilderFactoryImpl factory; @@ -80,7 +83,7 @@ class NetconfClientConfigurationBuilderFactoryImplTest { .setBackoffMultiplier(Decimal64.valueOf("1.5")) .setConnectionTimeoutMillis(Uint32.valueOf(20000)); factory = new NetconfClientConfigurationBuilderFactoryImpl(encryptionService, credentialProvider, - sslHandlerFactoryProvider); + sslContextFactoryProvider); } private void assertConfig(final NetconfClientConfiguration config) { @@ -117,7 +120,8 @@ class NetconfClientConfigurationBuilderFactoryImplTest { @Test void testTls() { - doReturn(sslHandlerFactory).when(sslHandlerFactoryProvider).getSslHandlerFactory(any()); + doReturn(sslContextFactory).when(sslContextFactoryProvider).getSslContextFactory(any()); + doReturn(sslContext).when(sslContextFactory).createSslContext(); final var config = createConfig( nodeBuilder.setTcpOnly(false).setProtocol(new ProtocolBuilder().setName(Name.TLS).build()).build()); assertConfig(config); diff --git a/apps/netconf-topology/src/test/java/org/opendaylight/netconf/topology/spi/NetconfNodeHandlerTest.java b/apps/netconf-topology/src/test/java/org/opendaylight/netconf/topology/spi/NetconfNodeHandlerTest.java index 8c05becd2d..1442988cce 100644 --- a/apps/netconf-topology/src/test/java/org/opendaylight/netconf/topology/spi/NetconfNodeHandlerTest.java +++ b/apps/netconf-topology/src/test/java/org/opendaylight/netconf/topology/spi/NetconfNodeHandlerTest.java @@ -52,7 +52,7 @@ import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId; import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceServices; import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceServices.Rpcs; import org.opendaylight.netconf.client.mdsal.api.SchemaResourceManager; -import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider; +import org.opendaylight.netconf.client.mdsal.api.SslContextFactoryProvider; import org.opendaylight.netconf.client.mdsal.impl.DefaultBaseNetconfSchemas; import org.opendaylight.netconf.common.NetconfTimer; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host; @@ -90,7 +90,7 @@ public class NetconfNodeHandlerTest { // DefaultNetconfClientConfigurationBuilderFactory setup @Mock - private SslHandlerFactoryProvider sslHandlerFactoryProvider; + private SslContextFactoryProvider sslContextFactoryProvider; @Mock private AAAEncryptionService encryptionService; @Mock @@ -136,7 +136,7 @@ public class NetconfNodeHandlerTest { // Instantiate the handler handler = new NetconfNodeHandler(clientFactory, timer, BASE_SCHEMAS, schemaManager, schemaAssembler, new NetconfClientConfigurationBuilderFactoryImpl(encryptionService, credentialProvider, - sslHandlerFactoryProvider), + sslContextFactoryProvider), deviceActionFactory, delegate, DEVICE_ID, NODE_ID, new NetconfNodeBuilder() .setHost(new Host(new IpAddress(new Ipv4Address("127.0.0.1")))) .setPort(new PortNumber(Uint16.valueOf(9999))) diff --git a/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/api/SslHandlerFactoryProvider.java b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/api/SslContextFactoryProvider.java similarity index 73% rename from plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/api/SslHandlerFactoryProvider.java rename to plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/api/SslContextFactoryProvider.java index 4d0061eff6..07a1257501 100644 --- a/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/api/SslHandlerFactoryProvider.java +++ b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/api/SslContextFactoryProvider.java @@ -9,21 +9,21 @@ package org.opendaylight.netconf.client.mdsal.api; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import org.opendaylight.netconf.client.SslHandlerFactory; +import org.opendaylight.netconf.client.SslContextFactory; import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.protocol.Specification; /** - * A provider for {@link SslHandlerFactory} implementations. This allows the factory to be tailored with a + * A provider for {@link SslContextFactory} implementations. This allows the factory to be tailored with a * {@link Specification}. */ -public interface SslHandlerFactoryProvider { +public interface SslContextFactoryProvider { /** - * Return a {@link SslHandlerFactory}, optionally conforming to a particular specification. + * Return a {@link SslContextFactory}, optionally conforming to a particular specification. * * @param specification A {@link Specification}, may be {@code null} - * @return A {@link SslHandlerFactory} + * @return A {@link SslContextFactory} * @throws IllegalArgumentException if {@code specification} is not {@code null} and it is not supported by this * provider. */ - @NonNull SslHandlerFactory getSslHandlerFactory(@Nullable Specification specification); + @NonNull SslContextFactory getSslContextFactory(@Nullable Specification specification); } diff --git a/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslContextFactory.java b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslContextFactory.java new file mode 100644 index 0000000000..a0379ecfc6 --- /dev/null +++ b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslContextFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 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.client.mdsal.impl; + +import static java.util.Objects.requireNonNull; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.JdkSslContext; +import io.netty.handler.ssl.SslContext; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Set; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.netconf.client.SslContextFactory; + +class DefaultSslContextFactory implements SslContextFactory { + private final DefaultSslContextFactoryProvider keyStoreProvider; + + DefaultSslContextFactory(final DefaultSslContextFactoryProvider keyStoreProvider) { + this.keyStoreProvider = requireNonNull(keyStoreProvider); + } + + @Override + public final SslContext createSslContext(final Set allowedKeys) { + final SSLContext sslContext; + try { + final var keyStore = keyStoreProvider.getJavaKeyStore(allowedKeys); + + final var kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, "".toCharArray()); + + final var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + + sslContext = SSLContext.getInstance("TLS"); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + } catch (IOException | GeneralSecurityException e) { + throw new IllegalStateException("Failed to initialize SSL context", e); + } + + return wrapSslContext(new JdkSslContext(sslContext, true, ClientAuth.NONE)); + } + + @NonNull SslContext wrapSslContext(final @NonNull SslContext sslContext) { + return sslContext; + } +} diff --git a/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslHandlerFactoryProvider.java b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslContextFactoryProvider.java similarity index 83% rename from plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslHandlerFactoryProvider.java rename to plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslContextFactoryProvider.java index 0d98fa0bb0..5b8ba2fe70 100644 --- a/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslHandlerFactoryProvider.java +++ b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslContextFactoryProvider.java @@ -19,8 +19,8 @@ import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.inject.Singleton; import org.eclipse.jdt.annotation.NonNull; -import org.opendaylight.netconf.client.SslHandlerFactory; -import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider; +import org.opendaylight.netconf.client.SslContextFactory; +import org.opendaylight.netconf.client.mdsal.api.SslContextFactoryProvider; import org.opendaylight.netconf.keystore.legacy.NetconfKeystore; import org.opendaylight.netconf.keystore.legacy.NetconfKeystoreService; import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.protocol.Specification; @@ -32,19 +32,19 @@ import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; @Singleton -@Component(service = SslHandlerFactoryProvider.class) -public final class DefaultSslHandlerFactoryProvider implements SslHandlerFactoryProvider, AutoCloseable { +@Component(service = SslContextFactoryProvider.class) +public final class DefaultSslContextFactoryProvider implements SslContextFactoryProvider, AutoCloseable { private static final X509Certificate[] EMPTY_CERTS = { }; private static final char[] EMPTY_CHARS = { }; - private final @NonNull SslHandlerFactory nospecFactory = new SslHandlerFactoryImpl(this, Set.of()); + private final @NonNull DefaultSslContextFactory nospecFactory = new DefaultSslContextFactory(this); private final Registration reg; private volatile @NonNull NetconfKeystore keystore = NetconfKeystore.EMPTY; @Inject @Activate - public DefaultSslHandlerFactoryProvider(@Reference final NetconfKeystoreService keystoreService) { + public DefaultSslContextFactoryProvider(@Reference final NetconfKeystoreService keystoreService) { reg = keystoreService.registerKeystoreConsumer(this::onKeystoreUpdated); } @@ -60,16 +60,16 @@ public final class DefaultSslHandlerFactoryProvider implements SslHandlerFactory } @Override - public SslHandlerFactory getSslHandlerFactory(final Specification specification) { + public SslContextFactory getSslContextFactory(final Specification specification) { if (specification == null) { return nospecFactory; - } - if (specification instanceof TlsCase tlsSpecification) { + } else if (specification instanceof TlsCase tlsSpecification) { final var excludedVersions = tlsSpecification.nonnullTls().getExcludedVersions(); return excludedVersions == null || excludedVersions.isEmpty() ? nospecFactory - : new SslHandlerFactoryImpl(this, excludedVersions); + : new FilteredSslContextFactory(this, excludedVersions); + } else { + throw new IllegalArgumentException("Cannot get TLS specification from: " + specification); } - throw new IllegalArgumentException("Cannot get TLS specification from: " + specification); } /** diff --git a/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/FilteredSslContext.java b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/FilteredSslContext.java new file mode 100644 index 0000000000..623399b37d --- /dev/null +++ b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/FilteredSslContext.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 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.client.mdsal.impl; + +import static java.util.Objects.requireNonNull; + +import io.netty.handler.ssl.DelegatingSslContext; +import io.netty.handler.ssl.SslContext; +import java.util.Arrays; +import java.util.Set; +import javax.net.ssl.SSLEngine; + +final class FilteredSslContext extends DelegatingSslContext { + private final Set excludedVersions; + + FilteredSslContext(final SslContext ctx, final Set excludedVersions) { + super(ctx); + this.excludedVersions = requireNonNull(excludedVersions); + } + + @Override + protected void initEngine(final SSLEngine engine) { + engine.setEnabledProtocols(Arrays.stream(engine.getSupportedProtocols()) + .filter(protocol -> !excludedVersions.contains(protocol)) + .toArray(String[]::new)); + engine.setEnabledCipherSuites(engine.getSupportedCipherSuites()); + engine.setEnableSessionCreation(true); + } +} diff --git a/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/FilteredSslContextFactory.java b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/FilteredSslContextFactory.java new file mode 100644 index 0000000000..481e75367f --- /dev/null +++ b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/FilteredSslContextFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 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.client.mdsal.impl; + +import static java.util.Objects.requireNonNull; + +import io.netty.handler.ssl.SslContext; +import java.util.Set; + +final class FilteredSslContextFactory extends DefaultSslContextFactory { + private final Set excludedVersions; + + FilteredSslContextFactory(final DefaultSslContextFactoryProvider keyStoreProvider, + final Set excludedVersions) { + super(keyStoreProvider); + this.excludedVersions = requireNonNull(excludedVersions); + } + + @Override + SslContext wrapSslContext(final SslContext sslContext) { + return new FilteredSslContext(sslContext, excludedVersions); + } +} diff --git a/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/SslHandlerFactoryImpl.java b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/SslHandlerFactoryImpl.java deleted file mode 100644 index 5d57d00d2f..0000000000 --- a/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/SslHandlerFactoryImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2019 Pantheon Technologies, 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.client.mdsal.impl; - -import static java.util.Objects.requireNonNull; - -import com.google.common.collect.Sets; -import io.netty.handler.ssl.SslHandler; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.util.Set; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.TrustManagerFactory; -import org.opendaylight.netconf.client.SslHandlerFactory; - -final class SslHandlerFactoryImpl implements SslHandlerFactory { - private final DefaultSslHandlerFactoryProvider keyStoreProvider; - private final Set excludedVersions; - - SslHandlerFactoryImpl(final DefaultSslHandlerFactoryProvider keyStoreProvider, final Set excludedVersions) { - this.keyStoreProvider = requireNonNull(keyStoreProvider); - this.excludedVersions = requireNonNull(excludedVersions); - } - - @Override - public SslHandler createSslHandler(final Set allowedKeys) { - try { - final KeyStore keyStore = keyStoreProvider.getJavaKeyStore(allowedKeys); - - final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(keyStore, "".toCharArray()); - - final TrustManagerFactory tmf = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(keyStore); - - final SSLContext sslCtx = SSLContext.getInstance("TLS"); - sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - final SSLEngine engine = sslCtx.createSSLEngine(); - engine.setUseClientMode(true); - - final String[] engineProtocols = engine.getSupportedProtocols(); - final String[] enabledProtocols; - if (!excludedVersions.isEmpty()) { - final var protocols = Sets.newHashSet(engineProtocols); - protocols.removeAll(excludedVersions); - enabledProtocols = protocols.toArray(new String[0]); - } else { - enabledProtocols = engineProtocols; - } - - engine.setEnabledProtocols(enabledProtocols); - engine.setEnabledCipherSuites(engine.getSupportedCipherSuites()); - engine.setEnableSessionCreation(true); - return new SslHandler(engine); - } catch (GeneralSecurityException | IOException exc) { - throw new IllegalStateException(exc); - } - } -} \ No newline at end of file diff --git a/plugins/netconf-client-mdsal/src/test/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslHandlerFactoryProviderTest.java b/plugins/netconf-client-mdsal/src/test/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslContextFactoryProviderTest.java similarity index 97% rename from plugins/netconf-client-mdsal/src/test/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslHandlerFactoryProviderTest.java rename to plugins/netconf-client-mdsal/src/test/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslContextFactoryProviderTest.java index 5f37ba205e..e42e451f0d 100644 --- a/plugins/netconf-client-mdsal/src/test/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslHandlerFactoryProviderTest.java +++ b/plugins/netconf-client-mdsal/src/test/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslContextFactoryProviderTest.java @@ -46,7 +46,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; @ExtendWith(MockitoExtension.class) -class DefaultSslHandlerFactoryProviderTest { +class DefaultSslContextFactoryProviderTest { private static final String XML_ELEMENT_PRIVATE_KEY = "private-key"; private static final String XML_ELEMENT_NAME = "name"; private static final String XML_ELEMENT_DATA = "data"; @@ -95,8 +95,8 @@ class DefaultSslHandlerFactoryProviderTest { keystore.close(); } - private DefaultSslHandlerFactoryProvider newProvider() { - return new DefaultSslHandlerFactoryProvider(keystore); + private DefaultSslContextFactoryProvider newProvider() { + return new DefaultSslContextFactoryProvider(keystore); } @Test @@ -209,6 +209,6 @@ class DefaultSslHandlerFactoryProviderTest { private static Document readKeystoreXML() throws Exception { return XmlUtil.readXmlToDocument( - DefaultSslHandlerFactoryProviderTest.class.getResourceAsStream("/netconf-keystore.xml")); + DefaultSslContextFactoryProviderTest.class.getResourceAsStream("/netconf-keystore.xml")); } } diff --git a/protocol/netconf-client/src/main/java/org/opendaylight/netconf/client/NetconfClientFactoryImpl.java b/protocol/netconf-client/src/main/java/org/opendaylight/netconf/client/NetconfClientFactoryImpl.java index ab6e7e6356..0ad6ae59fe 100644 --- a/protocol/netconf-client/src/main/java/org/opendaylight/netconf/client/NetconfClientFactoryImpl.java +++ b/protocol/netconf-client/src/main/java/org/opendaylight/netconf/client/NetconfClientFactoryImpl.java @@ -26,6 +26,7 @@ import org.opendaylight.netconf.transport.api.TransportChannelListener; import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException; import org.opendaylight.netconf.transport.ssh.SSHTransportStackFactory; import org.opendaylight.netconf.transport.tcp.TCPClient; +import org.opendaylight.netconf.transport.tls.FixedSslHandlerFactory; import org.opendaylight.netconf.transport.tls.TLSClient; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Uri; import org.osgi.service.component.annotations.Activate; @@ -73,7 +74,7 @@ public final class NetconfClientFactoryImpl implements NetconfClientFactory { } else if (TLS.equals(protocol)) { if (configuration.getTlsParameters() != null) { TLSClient.connect(new ClientTransportChannelListener(future, channelInitializer), bootstrap, - configuration.getTcpParameters(), configuration.getTlsParameters()); + configuration.getTcpParameters(), new FixedSslHandlerFactory(configuration.getTlsParameters())); } else { TLSClient.connect(new ClientTransportChannelListener(future, channelInitializer), bootstrap, configuration.getTcpParameters(), configuration.getSslHandlerFactory()); diff --git a/protocol/netconf-client/src/main/java/org/opendaylight/netconf/client/SslContextFactory.java b/protocol/netconf-client/src/main/java/org/opendaylight/netconf/client/SslContextFactory.java new file mode 100644 index 0000000000..986e17c6a3 --- /dev/null +++ b/protocol/netconf-client/src/main/java/org/opendaylight/netconf/client/SslContextFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2018 ZTE Corporation. 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.client; + +import com.google.common.annotations.Beta; +import io.netty.handler.ssl.SslContext; +import java.util.Set; + +/** + * Basic interface for instantiating a {@link SslContext}. Used to establish TSL connection. + */ +@Beta +public interface SslContextFactory { + /** + * This factory is used by the TLS client to create SslHandler that will be added into the channel pipeline when + * the channel is active. + */ + default SslContext createSslContext() { + return createSslContext(Set.of()); + } + + SslContext createSslContext(Set allowedKeys); +} diff --git a/protocol/netconf-client/src/main/java/org/opendaylight/netconf/client/SslHandlerFactory.java b/protocol/netconf-client/src/main/java/org/opendaylight/netconf/client/SslHandlerFactory.java deleted file mode 100644 index e71315615d..0000000000 --- a/protocol/netconf-client/src/main/java/org/opendaylight/netconf/client/SslHandlerFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2018 ZTE Corporation. 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.client; - -import io.netty.handler.ssl.SslHandler; -import java.util.Set; - -/** - * Basic interface for {@link SslHandler} builder. Used to establish TSL connection. - * - * @deprecated due to design change. SslHandler will be created dynamically based on TLS layer configuration - * {@link org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.TlsClientGrouping} - * by {@link NetconfClientFactory}. - */ -@Deprecated -public interface SslHandlerFactory { - /** - * This factory is used by the TLS client to create SslHandler that will be added into the channel pipeline when - * the channel is active. - */ - default SslHandler createSslHandler() { - return createSslHandler(Set.of()); - } - - SslHandler createSslHandler(Set allowedKeys); -} diff --git a/protocol/netconf-client/src/test/java/org/opendaylight/netconf/client/NetconfClientFactoryImplTest.java b/protocol/netconf-client/src/test/java/org/opendaylight/netconf/client/NetconfClientFactoryImplTest.java index 142009b7ba..c85bc1839b 100644 --- a/protocol/netconf-client/src/test/java/org/opendaylight/netconf/client/NetconfClientFactoryImplTest.java +++ b/protocol/netconf-client/src/test/java/org/opendaylight/netconf/client/NetconfClientFactoryImplTest.java @@ -56,6 +56,7 @@ import org.opendaylight.netconf.transport.ssh.ClientFactoryManagerConfigurator; import org.opendaylight.netconf.transport.ssh.SSHTransportStackFactory; import org.opendaylight.netconf.transport.ssh.ServerFactoryManagerConfigurator; import org.opendaylight.netconf.transport.tcp.TCPServer; +import org.opendaylight.netconf.transport.tls.FixedSslHandlerFactory; import org.opendaylight.netconf.transport.tls.TLSServer; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.crypt.hash.rev140806.CryptHash; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.RsaPrivateKeyFormat; @@ -168,12 +169,12 @@ class NetconfClientFactoryImplTest { final var clientContext = SslContextBuilder.forClient().keyManager(keyMgr).trustManager(trustMgr).build(); final var server = TLSServer.listen(serverTransportListener, SERVER_FACTORY.newServerBootstrap(), - tcpServerParams, channel -> serverContext.newHandler(channel.alloc())).get(1, TimeUnit.SECONDS); + tcpServerParams, new FixedSslHandlerFactory(serverContext)).get(1, TimeUnit.SECONDS); try { final var clientConfig = NetconfClientConfigurationBuilder.create() .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TLS) .withTcpParameters(tcpClientParams) - .withSslHandlerFactory(channel -> clientContext.newHandler(channel.alloc())) + .withSslHandlerFactory(new FixedSslHandlerFactory(clientContext)) .withSessionListener(sessionListener).build(); assertNotNull(factory.createClient(clientConfig)); verify(serverTransportListener, timeout(1000L)) diff --git a/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/FixedSslHandlerFactory.java b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/FixedSslHandlerFactory.java new file mode 100644 index 0000000000..ab6f16b190 --- /dev/null +++ b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/FixedSslHandlerFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 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 static java.util.Objects.requireNonNull; + +import io.netty.handler.ssl.SslContext; +import java.net.SocketAddress; +import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.TlsClientGrouping; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.TlsServerGrouping; + +public final class FixedSslHandlerFactory extends SslHandlerFactory { + private final SslContext sslContext; + + public FixedSslHandlerFactory(final SslContext sslContext) { + this.sslContext = requireNonNull(sslContext); + } + + public FixedSslHandlerFactory(final TlsClientGrouping clientParams) throws UnsupportedConfigurationException { + this(createSslContext(clientParams)); + } + + public FixedSslHandlerFactory(final TlsServerGrouping serverParams) throws UnsupportedConfigurationException { + this(createSslContext(serverParams)); + } + + @Override + protected SslContext getSslContext(final SocketAddress remoteAddress) { + return sslContext; + } +} diff --git a/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/KeyStoreUtils.java b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/KeyStoreUtils.java index 59f5c303fc..bebfad935b 100644 --- a/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/KeyStoreUtils.java +++ b/transport/transport-tls/src/main/java/org/opendaylight/netconf/transport/tls/KeyStoreUtils.java @@ -34,7 +34,7 @@ final class KeyStoreUtils { * @return key store instance * @throws UnsupportedConfigurationException if key store cannot be instantiated */ - static KeyStore newKeyStore() throws UnsupportedConfigurationException { + static @NonNull KeyStore newKeyStore() throws UnsupportedConfigurationException { final KeyStore keyStore; try { keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 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 index f216aca1f1..a079b8b469 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 PANTHEON.tech s.r.o. and others. All rights reserved. + * 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, @@ -7,22 +7,285 @@ */ package org.opendaylight.netconf.transport.tls; -import com.google.common.annotations.Beta; +import static org.opendaylight.netconf.transport.tls.ConfigUtils.setAsymmetricKey; +import static org.opendaylight.netconf.transport.tls.ConfigUtils.setEndEntityCertificateWithKey; +import static org.opendaylight.netconf.transport.tls.ConfigUtils.setX509Certificates; +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 com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import io.netty.channel.Channel; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslHandler; +import java.net.SocketAddress; +import java.security.KeyStore; +import java.util.List; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManagerFactory; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.CipherSuiteAlgBase; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsAes128CcmSha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsAes128GcmSha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsAes256GcmSha384; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsChacha20Poly1305Sha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes128Ccm; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes128GcmSha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes256Ccm; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes256GcmSha384; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithChacha20Poly1305Sha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes128Ccm; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes128GcmSha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes256Ccm; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes256GcmSha384; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithChacha20Poly1305Sha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheEcdsaWithAes128GcmSha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheEcdsaWithAes256GcmSha384; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheEcdsaWithChacha20Poly1305Sha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithAes128CcmSha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithAes128GcmSha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithAes256GcmSha384; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithChacha20Poly1305Sha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheRsaWithAes128GcmSha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheRsaWithAes256GcmSha384; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheRsaWithChacha20Poly1305Sha256; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228.InlineOrKeystoreAsymmetricKeyGrouping; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228.InlineOrKeystoreEndEntityCertWithKeyGrouping; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.TlsClientGrouping; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.tls.client.grouping.client.identity.auth.type.Certificate; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.tls.client.grouping.client.identity.auth.type.RawPublicKey; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.common.rev231228.HelloParamsGrouping; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.common.rev231228.TlsVersionBase; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.TlsServerGrouping; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.tls.server.grouping.server.identity.auth.type.RawPrivateKey; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore.rev231228.InlineOrTruststoreCertsGrouping; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore.rev231228.InlineOrTruststorePublicKeysGrouping; /** * Extension interface for external service integration with TLS transport. Used to build {@link TLSClient} and * {@link TLSServer} instances. */ -@Beta -@FunctionalInterface -public interface SslHandlerFactory { +public abstract class SslHandlerFactory { + private static final ImmutableMap CIPHER_SUITES = + ImmutableMap.builder() + .put(TlsAes128CcmSha256.VALUE, "TLS_AES_128_CCM_SHA256") + .put(TlsAes128GcmSha256.VALUE, "TLS_AES_128_GCM_SHA256") + .put(TlsAes256GcmSha384.VALUE, "TLS_AES_256_GCM_SHA384") + .put(TlsChacha20Poly1305Sha256.VALUE, "TLS_CHACHA20_POLY1305_SHA256") + .put(TlsDhePskWithAes128Ccm.VALUE, "TLS_DHE_PSK_WITH_AES_128_CCM") + .put(TlsDhePskWithAes128GcmSha256.VALUE, "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256") + .put(TlsDhePskWithAes256Ccm.VALUE, "TLS_DHE_PSK_WITH_AES_256_CCM") + .put(TlsDhePskWithAes256GcmSha384.VALUE, "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384") + .put(TlsDhePskWithChacha20Poly1305Sha256.VALUE, "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256") + .put(TlsDheRsaWithAes128Ccm.VALUE, "TLS_DHE_RSA_WITH_AES_128_CCM") + .put(TlsDheRsaWithAes128GcmSha256.VALUE, "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256") + .put(TlsDheRsaWithAes256Ccm.VALUE, "TLS_DHE_RSA_WITH_AES_256_CCM") + .put(TlsDheRsaWithAes256GcmSha384.VALUE, "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384") + .put(TlsDheRsaWithChacha20Poly1305Sha256.VALUE, "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256") + .put(TlsEcdheEcdsaWithAes128GcmSha256.VALUE, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") + .put(TlsEcdheEcdsaWithAes256GcmSha384.VALUE, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384") + .put(TlsEcdheEcdsaWithChacha20Poly1305Sha256.VALUE, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") + .put(TlsEcdhePskWithAes128CcmSha256.VALUE, "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256") + .put(TlsEcdhePskWithAes128GcmSha256.VALUE, "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256") + .put(TlsEcdhePskWithAes256GcmSha384.VALUE, "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384") + .put(TlsEcdhePskWithChacha20Poly1305Sha256.VALUE, "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256") + .put(TlsEcdheRsaWithAes128GcmSha256.VALUE, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") + .put(TlsEcdheRsaWithAes256GcmSha384.VALUE, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") + .put(TlsEcdheRsaWithChacha20Poly1305Sha256.VALUE, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") + .build(); + /** * Builds {@link SslHandler} instance for given {@link Channel}. * * @param channel channel - * @return A SslHandler + * @return A {@link SslHandler}, or {@code null} if the connection should be rejected */ - SslHandler createSslHandler(Channel channel); + public final @Nullable SslHandler createSslHandler(final @NonNull Channel channel) { + final var sslContext = getSslContext(channel.remoteAddress()); + return sslContext == null ? null : sslContext.newHandler(channel.alloc()); + } + + protected abstract @Nullable SslContext getSslContext(SocketAddress remoteAddress); + + protected static final @NonNull SslContext createSslContext(final @NonNull TlsClientGrouping clientParams) + throws UnsupportedConfigurationException { + final var builder = SslContextBuilder.forClient(); + + final var clientIdentity = clientParams.getClientIdentity(); + if (clientIdentity != null) { + final var authType = clientIdentity.getAuthType(); + if (authType instanceof Certificate cert) { + // if-feature "client-ident-x509-cert" + final var certificate = cert.getCertificate(); + if (certificate == null) { + throw new UnsupportedConfigurationException("Missing certificate in " + cert); + } + builder.keyManager(newKeyManager(certificate)); + } else if (authType instanceof RawPublicKey rawKey) { + // if-feature "client-ident-raw-public-key" + final var rawPrivateKey = rawKey.getRawPrivateKey(); + if (rawPrivateKey == null) { + throw new UnsupportedConfigurationException("Missing key in " + rawKey); + } + builder.keyManager(newKeyManager(rawPrivateKey)); + } else if (authType != null) { + throw new UnsupportedConfigurationException("Unsupported client authentication type " + authType); + } + } + + final var serverAuth = clientParams.getServerAuthentication(); + if (serverAuth != null) { + // CA && EE X509 certificates : if-feature "server-ident-x509-cert" + // Raw public key : if-feature "server-ident-raw-public-key" + final var trustManager = newTrustManager(serverAuth.getCaCerts(), serverAuth.getEeCerts(), + serverAuth.getRawPublicKeys()); + if (trustManager == null) { + throw new UnsupportedOperationException("No server authentication methods in " + serverAuth); + } + builder.trustManager(trustManager); + } + + return buildSslContext(builder, clientParams.getHelloParams()); + } + + protected static final @NonNull SslContext createSslContext(final @NonNull TlsServerGrouping serverParams) + throws UnsupportedConfigurationException { + final var serverIdentity = serverParams.getServerIdentity(); + if (serverIdentity == null) { + throw new UnsupportedConfigurationException("Missing server identity"); + } + final SslContextBuilder builder; + final var authType = serverIdentity.getAuthType(); + if (authType + instanceof org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228 + .tls.server.grouping.server.identity.auth.type.Certificate cert) { + // if-feature "server-ident-x509-cert" + final var certificate = cert.getCertificate(); + if (certificate == null) { + throw new UnsupportedConfigurationException("Missing certificate in " + cert); + } + builder = SslContextBuilder.forServer(newKeyManager(certificate)); + } else if (authType instanceof RawPrivateKey rawKey) { + // if-feature "server-ident-raw-public-key" + final var rawPrivateKey = rawKey.getRawPrivateKey(); + if (rawPrivateKey == null) { + throw new UnsupportedConfigurationException("Missing key in " + rawKey); + } + builder = SslContextBuilder.forServer(newKeyManager(rawPrivateKey)); + } else if (authType != null) { + throw new UnsupportedConfigurationException("Unsupported server authentication type " + authType); + } else { + throw new UnsupportedConfigurationException("Missing server authentication type"); + } + + final var clientAuth = serverParams.getClientAuthentication(); + if (clientAuth != null) { + // CA && EE Certs : if-feature "client-ident-x509-cert" + // Raw public keys : if-feature "client-ident-raw-public-key" + final var trustManager = newTrustManager(clientAuth.getCaCerts(), clientAuth.getEeCerts(), + clientAuth.getRawPublicKeys()); + if (trustManager == null) { + throw new UnsupportedOperationException("No client authentication methods in " + clientAuth); + } + builder.clientAuth(ClientAuth.REQUIRE).trustManager(trustManager); + } else { + builder.clientAuth(ClientAuth.NONE); + } + + return buildSslContext(builder, serverParams.getHelloParams()); + } + + // FIXME: should be TrustManagerBuilder + private static @Nullable TrustManagerFactory newTrustManager( + final @Nullable InlineOrTruststoreCertsGrouping caCerts, + final @Nullable InlineOrTruststoreCertsGrouping eeCerts, + final @Nullable InlineOrTruststorePublicKeysGrouping publicKeys) throws UnsupportedConfigurationException { + + if (publicKeys != null) { + // FIXME: implement this and advertize server-auth-raw-public-key from IetfTlsClientFeatureProvider + throw new UnsupportedConfigurationException("Public key authentication not implemented"); + } + if (caCerts != null || eeCerts != null) { + // X.509 certificates + final KeyStore keyStore = newKeyStore(); + setX509Certificates(keyStore, caCerts, eeCerts); + return buildTrustManagerFactory(keyStore); + } + return null; + } + + private static KeyManagerFactory newKeyManager( + final @NonNull InlineOrKeystoreEndEntityCertWithKeyGrouping endEntityCert) + throws UnsupportedConfigurationException { + final var keyStore = newKeyStore(); + setEndEntityCertificateWithKey(keyStore, endEntityCert); + return buildKeyManagerFactory(keyStore); + } + + private static KeyManagerFactory newKeyManager(final @NonNull InlineOrKeystoreAsymmetricKeyGrouping rawPrivateKey) + throws UnsupportedConfigurationException { + final var keyStore = newKeyStore(); + setAsymmetricKey(keyStore, rawPrivateKey); + return buildKeyManagerFactory(keyStore); + } + + private static @NonNull SslContext buildSslContext(final SslContextBuilder builder, + final HelloParamsGrouping helloParams) throws UnsupportedConfigurationException { + if (helloParams != null) { + final var tlsVersions = helloParams.getTlsVersions(); + if (tlsVersions != null) { + final var versions = tlsVersions.getTlsVersion(); + if (versions != null && !versions.isEmpty()) { + builder.protocols(createTlsStrings(versions)); + } + } + final var cipherSuites = helloParams.getCipherSuites(); + if (cipherSuites != null) { + final var ciphers = cipherSuites.getCipherSuite(); + if (ciphers != null && !ciphers.isEmpty()) { + builder.ciphers(createCipherStrings(ciphers)); + } + } + } + try { + return builder.build(); + } catch (SSLException e) { + throw new UnsupportedConfigurationException("Cannot instantiate TLS context", e); + } + } + + private static String[] createTlsStrings(final List versions) + throws UnsupportedConfigurationException { + // FIXME: cache these + final var ret = new String[versions.size()]; + int idx = 0; + for (var version : versions) { + final var str = IetfTlsCommonFeatureProvider.algorithmNameOf(version); + if (str == null) { + throw new UnsupportedConfigurationException("Unhandled TLS version " + version); + } + ret[idx++] = str; + } + return ret; + } + + private static ImmutableList createCipherStrings(final List ciphers) + throws UnsupportedConfigurationException { + // FIXME: cache these + final var builder = ImmutableList.builderWithExpectedSize(ciphers.size()); + for (var cipher : ciphers) { + final var str = CIPHER_SUITES.get(cipher); + if (str == null) { + throw new UnsupportedConfigurationException("Unhandled cipher suite " + cipher); + } + builder.add(str); + } + return builder.build(); + } } 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 92bb0868b9..78fa08ac19 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 @@ -11,7 +11,6 @@ import com.google.common.util.concurrent.ListenableFuture; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; import org.eclipse.jdt.annotation.NonNull; import org.opendaylight.netconf.transport.api.TransportChannelListener; import org.opendaylight.netconf.transport.api.TransportStack; @@ -20,9 +19,6 @@ import org.opendaylight.netconf.transport.tcp.TCPClient; import org.opendaylight.netconf.transport.tcp.TCPServer; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev231228.TcpClientGrouping; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev231228.TcpServerGrouping; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.TlsClientGrouping; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.tls.client.grouping.client.identity.auth.type.Certificate; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.tls.client.grouping.client.identity.auth.type.RawPublicKey; /** * A {@link TransportStack} acting as a TLS client. @@ -36,13 +32,6 @@ public final class TLSClient extends TLSTransportStack { super(listener, factory); } - public static @NonNull ListenableFuture connect(final TransportChannelListener listener, - final Bootstrap bootstrap, final TcpClientGrouping connectParams, final TlsClientGrouping clientParams) - 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 { @@ -50,58 +39,10 @@ public final class TLSClient extends TLSTransportStack { 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 { - final var client = newClient(listener, clientParams); - return transformUnderlay(client, TCPServer.listen(client.asListener(), bootstrap, listenParams)); - } - public static @NonNull ListenableFuture listen(final TransportChannelListener listener, final ServerBootstrap bootstrap, final TcpServerGrouping listenParams, final SslHandlerFactory factory) throws UnsupportedConfigurationException { final var client = new TLSClient(listener, factory); return transformUnderlay(client, TCPServer.listen(client.asListener(), bootstrap, listenParams)); } - - private static TLSClient newClient(final TransportChannelListener listener, final TlsClientGrouping clientParams) - throws UnsupportedConfigurationException { - final var builder = SslContextBuilder.forClient(); - - final var clientIdentity = clientParams.getClientIdentity(); - if (clientIdentity != null) { - final var authType = clientIdentity.getAuthType(); - if (authType instanceof Certificate cert) { - // if-feature "client-ident-x509-cert" - final var certificate = cert.getCertificate(); - if (certificate == null) { - throw new UnsupportedConfigurationException("Missing certificate in " + cert); - } - builder.keyManager(newKeyManager(certificate)); - } else if (authType instanceof RawPublicKey rawKey) { - // if-feature "client-ident-raw-public-key" - final var rawPrivateKey = rawKey.getRawPrivateKey(); - if (rawPrivateKey == null) { - throw new UnsupportedConfigurationException("Missing key in " + rawKey); - } - builder.keyManager(newKeyManager(rawPrivateKey)); - } else if (authType != null) { - throw new UnsupportedConfigurationException("Unsupported client authentication type " + authType); - } - } - - final var serverAuth = clientParams.getServerAuthentication(); - if (serverAuth != null) { - // CA && EE X509 certificates : if-feature "server-ident-x509-cert" - // Raw public key : if-feature "server-ident-raw-public-key" - final var trustManager = newTrustManager(serverAuth.getCaCerts(), serverAuth.getEeCerts(), - serverAuth.getRawPublicKeys()); - if (trustManager == null) { - throw new UnsupportedOperationException("No server authentication methods in " + serverAuth); - } - builder.trustManager(trustManager); - } - - return new TLSClient(listener, buildSslContext(builder, clientParams.getHelloParams())); - } } 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 b436289dca..bcaf0902e8 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 @@ -10,9 +10,7 @@ package org.opendaylight.netconf.transport.tls; import com.google.common.util.concurrent.ListenableFuture; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; -import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; import org.eclipse.jdt.annotation.NonNull; import org.opendaylight.netconf.transport.api.TransportChannelListener; import org.opendaylight.netconf.transport.api.TransportStack; @@ -21,9 +19,6 @@ import org.opendaylight.netconf.transport.tcp.TCPClient; import org.opendaylight.netconf.transport.tcp.TCPServer; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev231228.TcpClientGrouping; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev231228.TcpServerGrouping; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.TlsServerGrouping; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.tls.server.grouping.server.identity.auth.type.Certificate; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.tls.server.grouping.server.identity.auth.type.RawPrivateKey; /** * A {@link TransportStack} acting as a TLS server. @@ -37,13 +32,6 @@ public final class TLSServer extends TLSTransportStack { super(listener, factory); } - public static @NonNull ListenableFuture connect(final TransportChannelListener listener, - final Bootstrap bootstrap, final TcpClientGrouping connectParams, final TlsServerGrouping serverParams) - throws UnsupportedConfigurationException { - final var server = newServer(listener, serverParams); - return transformUnderlay(server, TCPClient.connect(server.asListener(), bootstrap, connectParams)); - } - public static @NonNull ListenableFuture connect(final TransportChannelListener listener, final Bootstrap bootstrap, final TcpClientGrouping connectParams, final SslHandlerFactory factory) throws UnsupportedConfigurationException { @@ -51,61 +39,10 @@ public final class TLSServer extends TLSTransportStack { 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 { - 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(); - if (serverIdentity == null) { - throw new UnsupportedConfigurationException("Missing server identity"); - } - final SslContextBuilder builder; - final var authType = serverIdentity.getAuthType(); - if (authType instanceof Certificate cert) { - // if-feature "server-ident-x509-cert" - final var certificate = cert.getCertificate(); - if (certificate == null) { - throw new UnsupportedConfigurationException("Missing certificate in " + cert); - } - builder = SslContextBuilder.forServer(newKeyManager(certificate)); - } else if (authType instanceof RawPrivateKey rawKey) { - // if-feature "server-ident-raw-public-key" - final var rawPrivateKey = rawKey.getRawPrivateKey(); - if (rawPrivateKey == null) { - throw new UnsupportedConfigurationException("Missing key in " + rawKey); - } - builder = SslContextBuilder.forServer(newKeyManager(rawPrivateKey)); - } else if (authType != null) { - throw new UnsupportedConfigurationException("Unsupported server authentication type " + authType); - } else { - throw new UnsupportedConfigurationException("Missing server authentication type"); - } - - final var clientAuth = serverParams.getClientAuthentication(); - if (clientAuth != null) { - // CA && EE Certs : if-feature "client-ident-x509-cert" - // Raw public keys : if-feature "client-ident-raw-public-key" - final var trustManager = newTrustManager(clientAuth.getCaCerts(), clientAuth.getEeCerts(), - clientAuth.getRawPublicKeys()); - if (trustManager == null) { - throw new UnsupportedOperationException("No client authentication methods in " + clientAuth); - } - builder.clientAuth(ClientAuth.REQUIRE).trustManager(trustManager); - } else { - builder.clientAuth(ClientAuth.NONE); - } - return new TLSServer(listener, buildSslContext(builder, serverParams.getHelloParams())); - } } 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 131d07983b..f6c09c3bec 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 @@ -8,98 +8,21 @@ package org.opendaylight.netconf.transport.tls; import static java.util.Objects.requireNonNull; -import static org.opendaylight.netconf.transport.tls.ConfigUtils.setAsymmetricKey; -import static org.opendaylight.netconf.transport.tls.ConfigUtils.setEndEntityCertificateWithKey; -import static org.opendaylight.netconf.transport.tls.ConfigUtils.setX509Certificates; -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 com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import java.security.KeyStore; -import java.util.List; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLException; -import javax.net.ssl.TrustManagerFactory; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.netconf.transport.api.AbstractOverlayTransportStack; import org.opendaylight.netconf.transport.api.TransportChannel; import org.opendaylight.netconf.transport.api.TransportChannelListener; -import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.CipherSuiteAlgBase; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsAes128CcmSha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsAes128GcmSha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsAes256GcmSha384; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsChacha20Poly1305Sha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes128Ccm; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes128GcmSha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes256Ccm; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes256GcmSha384; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithChacha20Poly1305Sha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes128Ccm; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes128GcmSha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes256Ccm; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes256GcmSha384; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithChacha20Poly1305Sha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheEcdsaWithAes128GcmSha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheEcdsaWithAes256GcmSha384; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheEcdsaWithChacha20Poly1305Sha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithAes128CcmSha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithAes128GcmSha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithAes256GcmSha384; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithChacha20Poly1305Sha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheRsaWithAes128GcmSha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheRsaWithAes256GcmSha384; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheRsaWithChacha20Poly1305Sha256; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228.InlineOrKeystoreAsymmetricKeyGrouping; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228.InlineOrKeystoreEndEntityCertWithKeyGrouping; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.common.rev231228.HelloParamsGrouping; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.common.rev231228.TlsVersionBase; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore.rev231228.InlineOrTruststoreCertsGrouping; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore.rev231228.InlineOrTruststorePublicKeysGrouping; /** * Base class for TLS TransportStacks. */ public abstract sealed class TLSTransportStack extends AbstractOverlayTransportStack permits TLSClient, TLSServer { - - private static final ImmutableMap CIPHER_SUITES = - ImmutableMap.builder() - .put(TlsAes128CcmSha256.VALUE, "TLS_AES_128_CCM_SHA256") - .put(TlsAes128GcmSha256.VALUE, "TLS_AES_128_GCM_SHA256") - .put(TlsAes256GcmSha384.VALUE, "TLS_AES_256_GCM_SHA384") - .put(TlsChacha20Poly1305Sha256.VALUE, "TLS_CHACHA20_POLY1305_SHA256") - .put(TlsDhePskWithAes128Ccm.VALUE, "TLS_DHE_PSK_WITH_AES_128_CCM") - .put(TlsDhePskWithAes128GcmSha256.VALUE, "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256") - .put(TlsDhePskWithAes256Ccm.VALUE, "TLS_DHE_PSK_WITH_AES_256_CCM") - .put(TlsDhePskWithAes256GcmSha384.VALUE, "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384") - .put(TlsDhePskWithChacha20Poly1305Sha256.VALUE, "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256") - .put(TlsDheRsaWithAes128Ccm.VALUE, "TLS_DHE_RSA_WITH_AES_128_CCM") - .put(TlsDheRsaWithAes128GcmSha256.VALUE, "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256") - .put(TlsDheRsaWithAes256Ccm.VALUE, "TLS_DHE_RSA_WITH_AES_256_CCM") - .put(TlsDheRsaWithAes256GcmSha384.VALUE, "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384") - .put(TlsDheRsaWithChacha20Poly1305Sha256.VALUE, "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256") - .put(TlsEcdheEcdsaWithAes128GcmSha256.VALUE, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") - .put(TlsEcdheEcdsaWithAes256GcmSha384.VALUE, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384") - .put(TlsEcdheEcdsaWithChacha20Poly1305Sha256.VALUE, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") - .put(TlsEcdhePskWithAes128CcmSha256.VALUE, "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256") - .put(TlsEcdhePskWithAes128GcmSha256.VALUE, "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256") - .put(TlsEcdhePskWithAes256GcmSha384.VALUE, "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384") - .put(TlsEcdhePskWithChacha20Poly1305Sha256.VALUE, "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256") - .put(TlsEcdheRsaWithAes128GcmSha256.VALUE, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") - .put(TlsEcdheRsaWithAes256GcmSha384.VALUE, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") - .put(TlsEcdheRsaWithChacha20Poly1305Sha256.VALUE, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") - .build(); - private final SslHandlerFactory factory; TLSTransportStack(final TransportChannelListener listener, final SslContext sslContext) { - this(listener, channel -> sslContext.newHandler(channel.alloc())); + this(listener, new FixedSslHandlerFactory(sslContext)); } TLSTransportStack(final TransportChannelListener listener, final SslHandlerFactory factory) { @@ -123,92 +46,4 @@ public abstract sealed class TLSTransportStack extends AbstractOverlayTransportS } }); } - - static KeyManagerFactory newKeyManager( - final @NonNull InlineOrKeystoreEndEntityCertWithKeyGrouping endEntityCert - ) throws UnsupportedConfigurationException { - final var keyStore = newKeyStore(); - setEndEntityCertificateWithKey(keyStore, endEntityCert); - return buildKeyManagerFactory(keyStore); - } - - static KeyManagerFactory newKeyManager(final @NonNull InlineOrKeystoreAsymmetricKeyGrouping rawPrivateKey) - throws UnsupportedConfigurationException { - final var keyStore = newKeyStore(); - setAsymmetricKey(keyStore, rawPrivateKey); - return buildKeyManagerFactory(keyStore); - } - - // FIXME: should be TrustManagerBuilder - protected static @Nullable TrustManagerFactory newTrustManager( - final @Nullable InlineOrTruststoreCertsGrouping caCerts, - final @Nullable InlineOrTruststoreCertsGrouping eeCerts, - final @Nullable InlineOrTruststorePublicKeysGrouping publicKeys) throws UnsupportedConfigurationException { - - if (publicKeys != null) { - // FIXME: implement this and advertize server-auth-raw-public-key from IetfTlsClientFeatureProvider - throw new UnsupportedConfigurationException("Public key authentication not implemented"); - } - if (caCerts != null || eeCerts != null) { - // X.509 certificates - final KeyStore keyStore = newKeyStore(); - setX509Certificates(keyStore, caCerts, eeCerts); - return buildTrustManagerFactory(keyStore); - } - return null; - } - - static SslContext buildSslContext(final SslContextBuilder builder, final HelloParamsGrouping helloParams) - throws UnsupportedConfigurationException { - if (helloParams != null) { - final var tlsVersions = helloParams.getTlsVersions(); - if (tlsVersions != null) { - final var versions = tlsVersions.getTlsVersion(); - if (versions != null && !versions.isEmpty()) { - builder.protocols(createTlsStrings(versions)); - } - } - final var cipherSuites = helloParams.getCipherSuites(); - if (cipherSuites != null) { - final var ciphers = cipherSuites.getCipherSuite(); - if (ciphers != null && !ciphers.isEmpty()) { - builder.ciphers(createCipherStrings(ciphers)); - } - } - } - try { - return builder.build(); - } catch (SSLException e) { - throw new UnsupportedConfigurationException("Cannot instantiate TLS context", e); - } - } - - private static String[] createTlsStrings(final List versions) - throws UnsupportedConfigurationException { - // FIXME: cache these - final var ret = new String[versions.size()]; - int idx = 0; - for (var version : versions) { - final var str = IetfTlsCommonFeatureProvider.algorithmNameOf(version); - if (str == null) { - throw new UnsupportedConfigurationException("Unhandled TLS version " + version); - } - ret[idx++] = str; - } - return ret; - } - - private static ImmutableList createCipherStrings(final List ciphers) - throws UnsupportedConfigurationException { - // FIXME: cache these - final var builder = ImmutableList.builderWithExpectedSize(ciphers.size()); - for (var cipher : ciphers) { - final var str = CIPHER_SUITES.get(cipher); - if (str == null) { - throw new UnsupportedConfigurationException("Unhandled cipher suite " + cipher); - } - builder.add(str); - } - return builder.build(); - } } 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 45f75378d2..a38a6475eb 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 @@ -182,9 +182,9 @@ class TlsClientServerTest { integrationTest( () -> TLSServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group), - tcpServerConfig, tlsServerConfig), + tcpServerConfig, new FixedSslHandlerFactory(tlsServerConfig)), () -> TLSClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group), - tcpClientConfig, tlsClientConfig) + tcpClientConfig, new FixedSslHandlerFactory(tlsClientConfig)) ); } @@ -201,9 +201,9 @@ class TlsClientServerTest { integrationTest( () -> TLSServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group), - tcpServerConfig, channel -> serverContext.newHandler(channel.alloc())), + tcpServerConfig, new FixedSslHandlerFactory(serverContext)), () -> TLSClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group), - tcpClientConfig, channel -> clientContext.newHandler(channel.alloc())) + tcpClientConfig, new FixedSslHandlerFactory(clientContext)) ); } @@ -255,14 +255,14 @@ class TlsClientServerTest { // start call-home client final var client = TLSClient.listen(clientListener, NettyTransportSupport.newServerBootstrap().group(group), - tcpServerConfig, channel -> clientContext.newHandler(channel.alloc())).get(2, TimeUnit.SECONDS); + tcpServerConfig, new FixedSslHandlerFactory(clientContext)).get(2, TimeUnit.SECONDS); try { // connect with call-home servers final var server1 = TLSServer.connect(serverListener, NettyTransportSupport.newBootstrap().group(group), - tcpClientConfig, channel -> serverContext.newHandler(channel.alloc())).get(2, TimeUnit.SECONDS); + tcpClientConfig, new FixedSslHandlerFactory(serverContext)).get(2, TimeUnit.SECONDS); final var server2 = TLSServer.connect(otherServerListener, NettyTransportSupport.newBootstrap().group(group), - tcpClientConfig, channel -> serverContext.newHandler(channel.alloc())).get(2, TimeUnit.SECONDS); + tcpClientConfig, new FixedSslHandlerFactory(serverContext)).get(2, TimeUnit.SECONDS); try { verify(serverListener, timeout(500)) .onTransportChannelEstablished(serverTransportChannelCaptor.capture()); @@ -272,9 +272,9 @@ class TlsClientServerTest { .onTransportChannelEstablished(clientTransportChannelCaptor.capture()); // extract channels sorted by server address var serverChannels = assertChannels(serverTransportChannelCaptor.getAllValues(), 2, - Comparator.comparing((Channel channel) -> channel.localAddress().toString())); + Comparator.comparing((final Channel channel) -> channel.localAddress().toString())); var clientChannels = assertChannels(clientTransportChannelCaptor.getAllValues(), 2, - Comparator.comparing((Channel channel) -> channel.remoteAddress().toString())); + Comparator.comparing((final Channel channel) -> channel.remoteAddress().toString())); for (int i = 0; i < 2; i++) { // validate channels are connecting same sockets assertEquals(serverChannels.get(i).remoteAddress(), clientChannels.get(i).localAddress());