Introduce TransportSsh{Client,Server} 33/107933/6
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 20 Sep 2023 15:42:26 +0000 (17:42 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Thu, 21 Sep 2023 09:44:50 +0000 (11:44 +0200)
We are using both SshClient and SshServer, configuring them extensively,
but we never start them.

Introduce proper specializations of SshClient and SshServer, which do
not allow themselves to be start or stopped. Strictly instantiate them
through provided builders, which also serve as hosts for configuration
adaptation.

This reduces clutter in SSH{Client,Server}, making them single-page
classes. Another benefit is that the consistency of SshClient/SshServer
is now checked, with defaults being correctly applied.

JIRA: NETCONF-590
Change-Id: If68714c21fe06de84cdff743c59c08f0f96f884c
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
transport/transport-ssh/pom.xml
transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/ConfigUtils.java
transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/SSHClient.java
transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/SSHServer.java
transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/TransportSshClient.java [new file with mode: 0644]
transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/TransportSshServer.java [new file with mode: 0644]

index dc4f5b67eb8874b665a6cc0f587418dc4f53cb26..fa2eb3a529b2361162d260eda3e7028b10b24fb8 100644 (file)
             <artifactId>commons-codec</artifactId>
             <version>1.15</version>
         </dependency>
+        <dependency>
+            <groupId>com.google.errorprone</groupId>
+            <artifactId>error_prone_annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
index c269465d5d5c30a3f9ed759cb9dec17d0ed6f1ff..fd97cff0f9a387b9033e9513a91ad1f7bde980df 100644 (file)
@@ -19,11 +19,10 @@ import java.util.List;
 import java.util.Map;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
-import org.opendaylight.netconf.shaded.sshd.client.ClientFactoryManager;
+import org.opendaylight.netconf.shaded.sshd.common.BaseBuilder;
 import org.opendaylight.netconf.shaded.sshd.common.FactoryManager;
 import org.opendaylight.netconf.shaded.sshd.common.kex.KeyExchangeFactory;
 import org.opendaylight.netconf.shaded.sshd.common.session.SessionHeartbeatController;
-import org.opendaylight.netconf.shaded.sshd.server.ServerFactoryManager;
 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.AsymmetricKeyPairGrouping;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.EcPrivateKeyFormat;
@@ -48,48 +47,18 @@ final class ConfigUtils {
         // utility class
     }
 
-    static void setTransportParams(final @NonNull ClientFactoryManager factoryMgr,
-            final @Nullable TransportParamsGrouping params) throws UnsupportedConfigurationException {
-        setTransportParams(factoryMgr, params, TransportUtils::getClientKexFactories);
-    }
-
-    static void setTransportParams(final @NonNull ServerFactoryManager factoryMgr,
-            final @Nullable TransportParamsGrouping params) throws UnsupportedConfigurationException {
-        setTransportParams(factoryMgr, params, TransportUtils::getServerKexFactories);
-    }
-
-    static void setTransportParams(final @NonNull FactoryManager factoryMgr,
+    static void setTransportParams(final @NonNull BaseBuilder<?, ?> builder,
             final @Nullable TransportParamsGrouping params, final @NonNull KexFactoryProvider kexProvider)
             throws UnsupportedConfigurationException {
-
-        factoryMgr.setCipherFactories(
-                TransportUtils.getCipherFactories(params == null ? null : params.getEncryption()));
-        factoryMgr.setSignatureFactories(
-                TransportUtils.getSignatureFactories(params == null ? null : params.getHostKey()));
-        factoryMgr.setKeyExchangeFactories(
-                kexProvider.getKexFactories(params == null ? null : params.getKeyExchange()));
-        factoryMgr.setMacFactories(
-                TransportUtils.getMacFactories(params == null ? null : params.getMac()));
-    }
-
-    static void setKeepAlives(final @NonNull ServerFactoryManager factoryMgr,
-            final org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417
-                    .ssh.server.grouping.Keepalives keepAlives) {
-        setKeepAlives(factoryMgr,
-                keepAlives == null ? null : keepAlives.getMaxWait(),
-                keepAlives == null ? null : keepAlives.getMaxAttempts());
-    }
-
-    static void setKeepAlives(final @NonNull ClientFactoryManager factoryMgr,
-            final org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417
-                    .ssh.client.grouping.Keepalives keepAlives) {
-        setKeepAlives(factoryMgr,
-                keepAlives == null ? null : keepAlives.getMaxWait(),
-                keepAlives == null ? null : keepAlives.getMaxAttempts());
+        builder
+            .cipherFactories(TransportUtils.getCipherFactories(params == null ? null : params.getEncryption()))
+            .signatureFactories(TransportUtils.getSignatureFactories(params == null ? null : params.getHostKey()))
+            .keyExchangeFactories(kexProvider.getKexFactories(params == null ? null : params.getKeyExchange()))
+            .macFactories(TransportUtils.getMacFactories(params == null ? null : params.getMac()));
     }
 
     @SuppressFBWarnings(value = "DLS_DEAD_LOCAL_STORE", justification = "maxAttempts usage need clarification")
-    private static void setKeepAlives(final @NonNull FactoryManager factoryMgr, final @Nullable Uint16 cfgMaxWait,
+    static void setKeepAlives(final @NonNull FactoryManager factoryMgr, final @Nullable Uint16 cfgMaxWait,
             final @Nullable Uint8 cfgMaxAttempts) {
         // FIXME: utilize max attempts
         final var maxAttempts = cfgMaxAttempts == null ? KEEP_ALIVE_DEFAULT_ATTEMPTS : cfgMaxAttempts.intValue();
@@ -252,7 +221,7 @@ final class ConfigUtils {
     }
 
     @FunctionalInterface
-    private interface KexFactoryProvider {
+    interface KexFactoryProvider {
         List<KeyExchangeFactory> getKexFactories(KeyExchange input) throws UnsupportedConfigurationException;
     }
 }
index 600cf372ae69203aff21ac57b7303a2f870f98ad..f28c1426e5ee2c62479cee11da50b39f881b838a 100644 (file)
@@ -7,38 +7,22 @@
  */
 package org.opendaylight.netconf.transport.ssh;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
 import io.netty.bootstrap.Bootstrap;
 import io.netty.bootstrap.ServerBootstrap;
 import io.netty.channel.group.DefaultChannelGroup;
 import io.netty.util.concurrent.GlobalEventExecutor;
-import java.security.cert.Certificate;
 import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.netconf.shaded.sshd.client.ClientFactoryManager;
-import org.opendaylight.netconf.shaded.sshd.client.SshClient;
-import org.opendaylight.netconf.shaded.sshd.client.auth.UserAuthFactory;
-import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.HostKeyIdentityProvider;
-import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.UserAuthHostBasedFactory;
-import org.opendaylight.netconf.shaded.sshd.client.auth.password.PasswordIdentityProvider;
-import org.opendaylight.netconf.shaded.sshd.client.auth.password.UserAuthPasswordFactory;
-import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
-import org.opendaylight.netconf.shaded.sshd.client.keyverifier.AcceptAllServerKeyVerifier;
 import org.opendaylight.netconf.shaded.sshd.client.session.ClientSessionImpl;
 import org.opendaylight.netconf.shaded.sshd.client.session.SessionFactory;
 import org.opendaylight.netconf.shaded.sshd.common.io.IoHandler;
-import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyIdentityProvider;
-import org.opendaylight.netconf.shaded.sshd.common.util.threads.ThreadUtils;
 import org.opendaylight.netconf.transport.api.TransportChannelListener;
 import org.opendaylight.netconf.transport.api.TransportStack;
 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
 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.crypto.types.rev230417.password.grouping.password.type.CleartextPassword;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.SshClientGrouping;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ClientIdentity;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ServerAuthentication;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev230417.TcpClientGrouping;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev230417.TcpServerGrouping;
 
@@ -74,96 +58,27 @@ public final class SSHClient extends SSHTransportStack {
     public static @NonNull ListenableFuture<SSHClient> connect(final TransportChannelListener listener,
             final Bootstrap bootstrap, final TcpClientGrouping connectParams,
             final SshClientGrouping clientParams) throws UnsupportedConfigurationException {
-        final var factoryMgr = newFactoryManager(clientParams);
-        final var sshClient = new SSHClient(listener, factoryMgr, getUsername(clientParams));
+        final var sshClient = newClient(listener, clientParams);
         return transformUnderlay(sshClient, TCPClient.connect(sshClient.asListener(), bootstrap, connectParams));
     }
 
     public static @NonNull ListenableFuture<SSHClient> listen(final TransportChannelListener listener,
             final ServerBootstrap bootstrap, final TcpServerGrouping listenParams, final SshClientGrouping clientParams)
             throws UnsupportedConfigurationException {
-        final var factoryMgr = newFactoryManager(clientParams);
-        final var sshClient = new SSHClient(listener, factoryMgr, getUsername(clientParams));
+        final var sshClient = newClient(listener, clientParams);
         return transformUnderlay(sshClient, TCPServer.listen(sshClient.asListener(), bootstrap, listenParams));
     }
 
-    private static String getUsername(final SshClientGrouping clientParams) {
-        final var clientIdentity = clientParams.getClientIdentity();
-        return clientIdentity == null ? "" : clientIdentity.getUsername();
-    }
-
-    private static ClientFactoryManager newFactoryManager(final SshClientGrouping parameters)
+    private static SSHClient newClient(final TransportChannelListener listener, final SshClientGrouping clientParams)
             throws UnsupportedConfigurationException {
-        final var factoryMgr = SshClient.setUpDefaultClient();
-
-        ConfigUtils.setTransportParams(factoryMgr, parameters.getTransportParams());
-        ConfigUtils.setKeepAlives(factoryMgr, parameters.getKeepalives());
-
-        setClientIdentity(factoryMgr, parameters.getClientIdentity());
-        setServerAuthentication(factoryMgr, parameters.getServerAuthentication());
-
-        factoryMgr.setServiceFactories(SshClient.DEFAULT_SERVICE_FACTORIES);
-        factoryMgr.setScheduledExecutorService(ThreadUtils.newSingleThreadScheduledExecutor("sshd-client-pool"));
-        return factoryMgr;
-    }
-
-    private static void setClientIdentity(@NonNull final ClientFactoryManager factoryMgr,
-            final @Nullable ClientIdentity clientIdentity) throws UnsupportedConfigurationException {
-        if (clientIdentity == null || clientIdentity.getNone() != null) {
-            return;
-        }
-        final var authFactoriesListBuilder = ImmutableList.<UserAuthFactory>builder();
-        final var password = clientIdentity.getPassword();
-        if (password != null) {
-            if (password.getPasswordType() instanceof CleartextPassword clearTextPassword) {
-                factoryMgr.setPasswordIdentityProvider(
-                        PasswordIdentityProvider.wrapPasswords(clearTextPassword.requireCleartextPassword()));
-                authFactoriesListBuilder.add(new UserAuthPasswordFactory());
-            }
-            // TODO support encrypted password -- requires augmentation of default schema
-        }
-        final var hostBased = clientIdentity.getHostbased();
-        if (hostBased != null) {
-            var keyPair = ConfigUtils.extractKeyPair(hostBased.getInlineOrKeystore());
-            var factory = new UserAuthHostBasedFactory();
-            factory.setClientHostKeys(HostKeyIdentityProvider.wrap(keyPair));
-            factory.setClientUsername(clientIdentity.getUsername());
-            factory.setClientHostname(null); // not provided via config
-            factory.setSignatureFactories(factoryMgr.getSignatureFactories());
-            authFactoriesListBuilder.add(factory);
-        }
-        final var publicKey = clientIdentity.getPublicKey();
-        if (publicKey != null) {
-            final var keyPairs = ConfigUtils.extractKeyPair(publicKey.getInlineOrKeystore());
-            factoryMgr.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
-            final var factory = new UserAuthPublicKeyFactory();
-            factory.setSignatureFactories(factoryMgr.getSignatureFactories());
-            authFactoriesListBuilder.add(factory);
-        }
-        // FIXME implement authentication using X509 certificate
-        final var userAuthFactories = authFactoriesListBuilder.build();
-        if (userAuthFactories.isEmpty()) {
-            throw new UnsupportedConfigurationException("Client Identity has no authentication mechanism defined");
-        }
-        factoryMgr.setUserAuthFactories(userAuthFactories);
-    }
+        final var clientIdentity = clientParams.getClientIdentity();
+        final var username = clientIdentity == null ? "" : clientIdentity.getUsername();
 
-    private static void setServerAuthentication(final @NonNull ClientFactoryManager factoryMgr,
-            final @Nullable ServerAuthentication serverAuthentication) throws UnsupportedConfigurationException {
-        if (serverAuthentication == null) {
-            factoryMgr.setServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE);
-            return;
-        }
-        final var certificatesList = ImmutableList.<Certificate>builder()
-                .addAll(ConfigUtils.extractCertificates(serverAuthentication.getCaCerts()))
-                .addAll(ConfigUtils.extractCertificates(serverAuthentication.getEeCerts()))
-                .build();
-        final var publicKeys = ConfigUtils.extractPublicKeys(serverAuthentication.getSshHostKeys());
-        if (!certificatesList.isEmpty() || !publicKeys.isEmpty()) {
-            factoryMgr.setServerKeyVerifier(new ServerPublicKeyVerifier(certificatesList, publicKeys));
-        } else {
-            throw new UnsupportedConfigurationException("Server authentication should contain either ssh-host-keys "
-                    + "or ca-certs or ee-certs");
-        }
+        return new SSHClient(listener, new TransportSshClient.Builder()
+            .transportParams(clientParams.getTransportParams())
+            .keepAlives(clientParams.getKeepalives())
+            .clientIdentity(clientParams.getClientIdentity())
+            .serverAuthentication(clientParams.getServerAuthentication())
+            .buildChecked(), username);
     }
 }
\ No newline at end of file
index d3e398c58bb5d6c9daed8aa4d25f1ed0ab4b7f5a..d11c1fdf51cf2990da4b672a29377208ea918558 100644 (file)
@@ -10,26 +10,14 @@ package org.opendaylight.netconf.transport.ssh;
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.ListenableFuture;
 import io.netty.bootstrap.Bootstrap;
 import io.netty.bootstrap.ServerBootstrap;
 import io.netty.channel.group.DefaultChannelGroup;
 import io.netty.util.concurrent.GlobalEventExecutor;
-import java.security.PublicKey;
-import java.util.List;
 import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.netconf.shaded.sshd.common.io.IoHandler;
-import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyPairProvider;
-import org.opendaylight.netconf.shaded.sshd.common.util.threads.ThreadUtils;
 import org.opendaylight.netconf.shaded.sshd.server.ServerFactoryManager;
-import org.opendaylight.netconf.shaded.sshd.server.SshServer;
-import org.opendaylight.netconf.shaded.sshd.server.auth.UserAuthFactory;
-import org.opendaylight.netconf.shaded.sshd.server.auth.hostbased.UserAuthHostBasedFactory;
-import org.opendaylight.netconf.shaded.sshd.server.auth.password.UserAuthPasswordFactory;
-import org.opendaylight.netconf.shaded.sshd.server.auth.pubkey.UserAuthPublicKeyFactory;
 import org.opendaylight.netconf.shaded.sshd.server.session.SessionFactory;
 import org.opendaylight.netconf.transport.api.TransportChannelListener;
 import org.opendaylight.netconf.transport.api.TransportStack;
@@ -37,8 +25,6 @@ import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
 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.ssh.server.rev230417.SshServerGrouping;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ClientAuthentication;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ServerIdentity;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev230417.TcpClientGrouping;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev230417.TcpServerGrouping;
 
@@ -66,16 +52,15 @@ public final class SSHServer extends SSHTransportStack {
 
     public static @NonNull ListenableFuture<SSHServer> connect(final TransportChannelListener listener,
             final Bootstrap bootstrap, final TcpClientGrouping connectParams, final SshServerGrouping serverParams)
-            throws UnsupportedConfigurationException {
-        final var server = new SSHServer(listener, newFactoryManager(requireNonNull(serverParams), null));
+                throws UnsupportedConfigurationException {
+        final var server = newServer(listener, requireNonNull(serverParams), null);
         return transformUnderlay(server, TCPClient.connect(server.asListener(), bootstrap, connectParams));
     }
 
     public static @NonNull ListenableFuture<SSHServer> listen(final TransportChannelListener listener,
             final ServerBootstrap bootstrap, final TcpServerGrouping connectParams,
             final SshServerGrouping serverParams) throws UnsupportedConfigurationException {
-        requireNonNull(serverParams);
-        return listen(listener, bootstrap, connectParams, serverParams, null);
+        return listen(listener, bootstrap, connectParams, requireNonNull(serverParams), null);
     }
 
     /**
@@ -94,97 +79,18 @@ public final class SSHServer extends SSHTransportStack {
     public static @NonNull ListenableFuture<SSHServer> listen(final TransportChannelListener listener,
             final ServerBootstrap bootstrap, final TcpServerGrouping connectParams,
             final SshServerGrouping serverParams, final ServerFactoryManagerConfigurator configurator)
-            throws UnsupportedConfigurationException {
+                throws UnsupportedConfigurationException {
         checkArgument(serverParams != null || configurator != null,
             "Neither server parameters nor factory configurator is defined");
-        final var factoryMgr = newFactoryManager(serverParams, configurator);
-        final var server = new SSHServer(listener, factoryMgr);
+        final var server = newServer(listener, serverParams, configurator);
         return transformUnderlay(server, TCPServer.listen(server.asListener(), bootstrap, connectParams));
     }
 
-    private static ServerFactoryManager newFactoryManager(final @Nullable SshServerGrouping serverParams,
-            final @Nullable ServerFactoryManagerConfigurator configurator) throws UnsupportedConfigurationException {
-        final var factoryMgr = SshServer.setUpDefaultServer();
-        if (serverParams != null) {
-            ConfigUtils.setTransportParams(factoryMgr, serverParams.getTransportParams());
-            ConfigUtils.setKeepAlives(factoryMgr, serverParams.getKeepalives());
-            setServerIdentity(factoryMgr, serverParams.getServerIdentity());
-            setClientAuthentication(factoryMgr, serverParams.getClientAuthentication());
-        }
-        if (configurator != null) {
-            configurator.configureServerFactoryManager(factoryMgr);
-        }
-        factoryMgr.setServiceFactories(SshServer.DEFAULT_SERVICE_FACTORIES);
-        factoryMgr.setScheduledExecutorService(ThreadUtils.newSingleThreadScheduledExecutor(""));
-        return factoryMgr;
-    }
-
-    private static void setServerIdentity(final @NonNull ServerFactoryManager factoryMgr,
-            final @Nullable ServerIdentity serverIdentity) throws UnsupportedConfigurationException {
-        if (serverIdentity == null) {
-            throw new UnsupportedConfigurationException("Server identity configuration is required");
-        }
-        final var hostKey = serverIdentity.getHostKey();
-        if (hostKey == null || hostKey.isEmpty()) {
-            throw new UnsupportedConfigurationException("Host keys is missing in server identity configuration");
-        }
-        final var serverHostKeyPairs = ConfigUtils.extractServerHostKeys(hostKey);
-        if (!serverHostKeyPairs.isEmpty()) {
-            factoryMgr.setKeyPairProvider(KeyPairProvider.wrap(serverHostKeyPairs));
-        }
-    }
-
-    private static void setClientAuthentication(final @NonNull ServerFactoryManager factoryMgr,
-            final @Nullable ClientAuthentication clientAuthentication) throws UnsupportedConfigurationException {
-        if (clientAuthentication == null) {
-            return;
-        }
-        final var users = clientAuthentication.getUsers();
-        if (users == null) {
-            return;
-        }
-        final var userMap = users.getUser();
-        if (userMap != null) {
-            final var passwordMapBuilder = ImmutableMap.<String, String>builder();
-            final var hostBasedMapBuilder = ImmutableMap.<String, List<PublicKey>>builder();
-            final var publicKeyMapBuilder = ImmutableMap.<String, List<PublicKey>>builder();
-            for (var entry : userMap.entrySet()) {
-                final String username = entry.getKey().getName();
-                final var value = entry.getValue();
-                final var password = value.getPassword();
-                if (password != null) {
-                    passwordMapBuilder.put(username, password.getValue());
-                }
-                final var hostBased = value.getHostbased();
-                if (hostBased != null) {
-                    hostBasedMapBuilder.put(username, ConfigUtils.extractPublicKeys(hostBased.getInlineOrTruststore()));
-                }
-                final var publicKey = value.getPublicKeys();
-                if (publicKey != null) {
-                    publicKeyMapBuilder.put(username, ConfigUtils.extractPublicKeys(publicKey.getInlineOrTruststore()));
-                }
-            }
-            final var authFactoriesBuilder = ImmutableList.<UserAuthFactory>builder();
-            final var passwordMap = passwordMapBuilder.build();
-            if (!passwordMap.isEmpty()) {
-                authFactoriesBuilder.add(new UserAuthPasswordFactory());
-                factoryMgr.setPasswordAuthenticator(new CryptHashPasswordAuthenticator(passwordMap));
-            }
-            final var hostBasedMap = hostBasedMapBuilder.build();
-            if (!hostBasedMap.isEmpty()) {
-                final var factory = new UserAuthHostBasedFactory();
-                factory.setSignatureFactories(factoryMgr.getSignatureFactories());
-                authFactoriesBuilder.add(factory);
-                factoryMgr.setHostBasedAuthenticator(new UserPublicKeyAuthenticator(hostBasedMap));
-            }
-            final var publicKeyMap = publicKeyMapBuilder.build();
-            if (!publicKeyMap.isEmpty()) {
-                final var factory = new UserAuthPublicKeyFactory();
-                factory.setSignatureFactories(factoryMgr.getSignatureFactories());
-                authFactoriesBuilder.add(factory);
-                factoryMgr.setPublickeyAuthenticator(new UserPublicKeyAuthenticator(publicKeyMap));
-            }
-            factoryMgr.setUserAuthFactories(authFactoriesBuilder.build());
-        }
+    private static SSHServer newServer(final TransportChannelListener listener, final SshServerGrouping serverParams,
+            final ServerFactoryManagerConfigurator configurator) throws UnsupportedConfigurationException {
+        return new SSHServer(listener, new TransportSshServer.Builder()
+            .serverParams(serverParams)
+            .configurator(configurator)
+            .buildChecked());
     }
 }
\ No newline at end of file
diff --git a/transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/TransportSshClient.java b/transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/TransportSshClient.java
new file mode 100644 (file)
index 0000000..1175eb8
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * 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.ssh;
+
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.DoNotCall;
+import java.security.cert.Certificate;
+import org.opendaylight.netconf.shaded.sshd.client.ClientBuilder;
+import org.opendaylight.netconf.shaded.sshd.client.SshClient;
+import org.opendaylight.netconf.shaded.sshd.client.auth.UserAuthFactory;
+import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.HostKeyIdentityProvider;
+import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.UserAuthHostBasedFactory;
+import org.opendaylight.netconf.shaded.sshd.client.auth.password.PasswordIdentityProvider;
+import org.opendaylight.netconf.shaded.sshd.client.auth.password.UserAuthPasswordFactory;
+import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
+import org.opendaylight.netconf.shaded.sshd.client.keyverifier.ServerKeyVerifier;
+import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyIdentityProvider;
+import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.password.grouping.password.type.CleartextPassword;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ClientIdentity;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.Keepalives;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ServerAuthentication;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.common.rev230417.TransportParamsGrouping;
+
+/**
+ * Our internal-use {@link SshClient}. We reuse all the properties and logic of an {@link SshClient}, but we never allow
+ * it to be started.
+ */
+final class TransportSshClient extends SshClient {
+    private TransportSshClient() {
+        // Hidden on purpose
+    }
+
+    /**
+     * Guaranteed to throw an exception.
+     *
+     * @throws UnsupportedOperationException always
+     */
+    @Override
+    @Deprecated(forRemoval = true)
+    @DoNotCall("Always throws UnsupportedOperationException")
+    public void start() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Guaranteed to throw an exception.
+     *
+     * @throws UnsupportedOperationException always
+     */
+    @Override
+    @Deprecated(forRemoval = true)
+    @DoNotCall("Always throws UnsupportedOperationException")
+    public void stop() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * A {@link ClientBuilder} producing {@link TransportSshClient}s. Also hosts adaptation from
+     * {@code ietf-netconf-client.yang} configuration.
+     */
+    static final class Builder extends ClientBuilder {
+        private Keepalives keepAlives;
+        private ClientIdentity clientIdentity;
+
+        Builder transportParams(final TransportParamsGrouping params) throws UnsupportedConfigurationException {
+            ConfigUtils.setTransportParams(this, params, TransportUtils::getClientKexFactories);
+            return this;
+        }
+
+        Builder keepAlives(final Keepalives newkeepAlives) {
+            keepAlives = newkeepAlives;
+            return this;
+        }
+
+        Builder clientIdentity(final ClientIdentity newClientIdentity) {
+            clientIdentity = newClientIdentity;
+            return this;
+        }
+
+        Builder serverAuthentication(final ServerAuthentication serverAuthentication)
+                throws UnsupportedConfigurationException {
+            final ServerKeyVerifier newVerifier;
+            if (serverAuthentication != null) {
+                final var certificatesList = ImmutableList.<Certificate>builder()
+                    .addAll(ConfigUtils.extractCertificates(serverAuthentication.getCaCerts()))
+                    .addAll(ConfigUtils.extractCertificates(serverAuthentication.getEeCerts()))
+                    .build();
+                final var publicKeys = ConfigUtils.extractPublicKeys(serverAuthentication.getSshHostKeys());
+                if (certificatesList.isEmpty() && publicKeys.isEmpty()) {
+                    throw new UnsupportedConfigurationException(
+                        "Server authentication should contain either ssh-host-keys, or ca-certs, or ee-certs");
+                }
+                newVerifier = new ServerPublicKeyVerifier(certificatesList, publicKeys);
+            } else {
+                newVerifier = null;
+            }
+
+            serverKeyVerifier(newVerifier);
+            return this;
+        }
+
+        TransportSshClient buildChecked() throws UnsupportedConfigurationException {
+            final var ret = (TransportSshClient) super.build(true);
+            if (keepAlives != null) {
+                ConfigUtils.setKeepAlives(ret, keepAlives.getMaxWait(), keepAlives.getMaxAttempts());
+            } else {
+                ConfigUtils.setKeepAlives(ret, null, null);
+            }
+            if (clientIdentity != null && clientIdentity.getNone() == null) {
+                setClientIdentity(ret, clientIdentity);
+            }
+
+            // FIXME: this is the default added by checkConfig(), but we really want to use an EventLoopGroup for this
+            // ret.setScheduledExecutorService(group);
+
+            try {
+                ret.checkConfig();
+            } catch (IllegalArgumentException e) {
+                throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
+            }
+            return ret;
+        }
+
+        /**
+         * Guaranteed to throw an exception.
+         *
+         * @throws UnsupportedOperationException always
+         */
+        @Override
+        @Deprecated(forRemoval = true)
+        @DoNotCall("Always throws UnsupportedOperationException")
+        public TransportSshClient build() {
+            throw new UnsupportedOperationException();
+        }
+
+        /**
+         * Guaranteed to throw an exception.
+         *
+         * @throws UnsupportedOperationException always
+         */
+        @Override
+        @Deprecated(forRemoval = true)
+        @DoNotCall("Always throws UnsupportedOperationException")
+        public TransportSshClient build(final boolean isFillWithDefaultValues) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        protected ClientBuilder fillWithDefaultValues() {
+            if (factory == null) {
+                factory = TransportSshClient::new;
+            }
+            return super.fillWithDefaultValues();
+        }
+
+        private static void setClientIdentity(final TransportSshClient client, final ClientIdentity clientIdentity)
+                throws UnsupportedConfigurationException {
+            final var authFactoriesListBuilder = ImmutableList.<UserAuthFactory>builder();
+            final var password = clientIdentity.getPassword();
+            if (password != null) {
+                if (password.getPasswordType() instanceof CleartextPassword clearTextPassword) {
+                    client.setPasswordIdentityProvider(
+                            PasswordIdentityProvider.wrapPasswords(clearTextPassword.requireCleartextPassword()));
+                    authFactoriesListBuilder.add(new UserAuthPasswordFactory());
+                }
+                // TODO support encrypted password -- requires augmentation of default schema
+            }
+            final var hostBased = clientIdentity.getHostbased();
+            if (hostBased != null) {
+                var keyPair = ConfigUtils.extractKeyPair(hostBased.getInlineOrKeystore());
+                var factory = new UserAuthHostBasedFactory();
+                factory.setClientHostKeys(HostKeyIdentityProvider.wrap(keyPair));
+                factory.setClientUsername(clientIdentity.getUsername());
+                factory.setClientHostname(null); // not provided via config
+                factory.setSignatureFactories(client.getSignatureFactories());
+                authFactoriesListBuilder.add(factory);
+            }
+            final var publicKey = clientIdentity.getPublicKey();
+            if (publicKey != null) {
+                final var keyPairs = ConfigUtils.extractKeyPair(publicKey.getInlineOrKeystore());
+                client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
+                final var factory = new UserAuthPublicKeyFactory();
+                factory.setSignatureFactories(client.getSignatureFactories());
+                authFactoriesListBuilder.add(factory);
+            }
+            // FIXME implement authentication using X509 certificate
+            final var userAuthFactories = authFactoriesListBuilder.build();
+            if (userAuthFactories.isEmpty()) {
+                throw new UnsupportedConfigurationException("Client Identity has no authentication mechanism defined");
+            }
+            client.setUserAuthFactories(userAuthFactories);
+        }
+    }
+}
diff --git a/transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/TransportSshServer.java b/transport/transport-ssh/src/main/java/org/opendaylight/netconf/transport/ssh/TransportSshServer.java
new file mode 100644 (file)
index 0000000..5cddbbc
--- /dev/null
@@ -0,0 +1,215 @@
+/*
+ * 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.ssh;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.DoNotCall;
+import java.security.PublicKey;
+import java.util.List;
+import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyPairProvider;
+import org.opendaylight.netconf.shaded.sshd.server.ServerBuilder;
+import org.opendaylight.netconf.shaded.sshd.server.SshServer;
+import org.opendaylight.netconf.shaded.sshd.server.auth.UserAuthFactory;
+import org.opendaylight.netconf.shaded.sshd.server.auth.hostbased.UserAuthHostBasedFactory;
+import org.opendaylight.netconf.shaded.sshd.server.auth.password.UserAuthPasswordFactory;
+import org.opendaylight.netconf.shaded.sshd.server.auth.pubkey.UserAuthPublicKeyFactory;
+import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.SshServerGrouping;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ClientAuthentication;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.Keepalives;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ServerIdentity;
+
+/**
+ * Our internal-use {@link SshServer}. We reuse all the properties and logic of an {@link SshServer}, but we never allow
+ * it to be started.
+ */
+final class TransportSshServer extends SshServer {
+    private TransportSshServer() {
+        // Hidden on purpose
+    }
+
+    /**
+     * Guaranteed to throw an exception.
+     *
+     * @throws UnsupportedOperationException always
+     */
+    @Override
+    @Deprecated(forRemoval = true)
+    @DoNotCall("Always throws UnsupportedOperationException")
+    public void start() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Guaranteed to throw an exception.
+     *
+     * @throws UnsupportedOperationException always
+     */
+    @Override
+    @Deprecated(forRemoval = true)
+    @DoNotCall("Always throws UnsupportedOperationException")
+    public void stop() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * A {@link ServerBuilder} producing {@link TransportSshServer}s. Also hosts adaptation from
+     * {@code ietf-netconf-server.yang} configuration.
+     */
+    static final class Builder extends ServerBuilder {
+        private ServerFactoryManagerConfigurator configurator;
+        private ClientAuthentication clientAuthentication;
+        private ServerIdentity serverIdentity;
+        private Keepalives keepAlives;
+
+        Builder serverParams(final SshServerGrouping serverParams) throws UnsupportedConfigurationException {
+            if (serverParams != null) {
+                ConfigUtils.setTransportParams(this, serverParams.getTransportParams(),
+                    TransportUtils::getServerKexFactories);
+                keepAlives = serverParams.getKeepalives();
+                serverIdentity = serverParams.getServerIdentity();
+                if (serverIdentity == null) {
+                    throw new UnsupportedConfigurationException("Server identity configuration is required");
+                }
+                clientAuthentication = serverParams.getClientAuthentication();
+            }
+            return this;
+        }
+
+        Builder configurator(final ServerFactoryManagerConfigurator newConfigurator) {
+            configurator = newConfigurator;
+            return this;
+        }
+
+        /**
+         * Guaranteed to throw an exception.
+         *
+         * @throws UnsupportedOperationException always
+         */
+        @Override
+        @Deprecated(forRemoval = true)
+        @DoNotCall("Always throws UnsupportedOperationException")
+        public TransportSshServer build() {
+            throw new UnsupportedOperationException();
+        }
+
+        /**
+         * Guaranteed to throw an exception.
+         *
+         * @throws UnsupportedOperationException always
+         */
+        @Override
+        @Deprecated(forRemoval = true)
+        @DoNotCall("Always throws UnsupportedOperationException")
+        public TransportSshServer build(final boolean isFillWithDefaultValues) {
+            throw new UnsupportedOperationException();
+        }
+
+        TransportSshServer buildChecked() throws UnsupportedConfigurationException {
+            final var ret = (TransportSshServer) super.build(true);
+            if (keepAlives != null) {
+                ConfigUtils.setKeepAlives(ret, keepAlives.getMaxWait(), keepAlives.getMaxAttempts());
+            } else {
+                ConfigUtils.setKeepAlives(ret, null, null);
+            }
+            if (serverIdentity != null) {
+                setServerIdentity(ret, serverIdentity);
+            }
+            if (clientAuthentication != null) {
+                setClientAuthentication(ret, clientAuthentication);
+            }
+            if (configurator != null) {
+                configurator.configureServerFactoryManager(ret);
+            }
+
+            // FIXME: this is the default added by checkConfig(), but we really want to use an EventLoopGroup for this
+            // ret.setScheduledExecutorService(group);
+
+            try {
+                ret.checkConfig();
+            } catch (IllegalArgumentException e) {
+                throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
+            }
+            return ret;
+        }
+
+        @Override
+        protected ServerBuilder fillWithDefaultValues() {
+            if (factory == null) {
+                factory = TransportSshServer::new;
+            }
+            return super.fillWithDefaultValues();
+        }
+
+        private static void setServerIdentity(final TransportSshServer server, final ServerIdentity serverIdentity)
+                throws UnsupportedConfigurationException {
+            final var hostKey = serverIdentity.getHostKey();
+            if (hostKey == null || hostKey.isEmpty()) {
+                throw new UnsupportedConfigurationException("Host keys is missing in server identity configuration");
+            }
+            final var serverHostKeyPairs = ConfigUtils.extractServerHostKeys(hostKey);
+            if (!serverHostKeyPairs.isEmpty()) {
+                server.setKeyPairProvider(KeyPairProvider.wrap(serverHostKeyPairs));
+            }
+        }
+
+        private static void setClientAuthentication(final TransportSshServer server,
+                final ClientAuthentication clientAuthentication) throws UnsupportedConfigurationException {
+            final var users = clientAuthentication.getUsers();
+            if (users == null) {
+                return;
+            }
+            final var userMap = users.getUser();
+            if (userMap != null) {
+                final var passwordMapBuilder = ImmutableMap.<String, String>builder();
+                final var hostBasedMapBuilder = ImmutableMap.<String, List<PublicKey>>builder();
+                final var publicKeyMapBuilder = ImmutableMap.<String, List<PublicKey>>builder();
+                for (var entry : userMap.entrySet()) {
+                    final var username = entry.getKey().getName();
+                    final var value = entry.getValue();
+                    final var password = value.getPassword();
+                    if (password != null) {
+                        passwordMapBuilder.put(username, password.getValue());
+                    }
+                    final var hostBased = value.getHostbased();
+                    if (hostBased != null) {
+                        hostBasedMapBuilder.put(username,
+                            ConfigUtils.extractPublicKeys(hostBased.getInlineOrTruststore()));
+                    }
+                    final var publicKey = value.getPublicKeys();
+                    if (publicKey != null) {
+                        publicKeyMapBuilder.put(username,
+                            ConfigUtils.extractPublicKeys(publicKey.getInlineOrTruststore()));
+                    }
+                }
+                final var authFactoriesBuilder = ImmutableList.<UserAuthFactory>builder();
+                final var passwordMap = passwordMapBuilder.build();
+                if (!passwordMap.isEmpty()) {
+                    authFactoriesBuilder.add(new UserAuthPasswordFactory());
+                    server.setPasswordAuthenticator(new CryptHashPasswordAuthenticator(passwordMap));
+                }
+                final var hostBasedMap = hostBasedMapBuilder.build();
+                if (!hostBasedMap.isEmpty()) {
+                    final var factory = new UserAuthHostBasedFactory();
+                    factory.setSignatureFactories(server.getSignatureFactories());
+                    authFactoriesBuilder.add(factory);
+                    server.setHostBasedAuthenticator(new UserPublicKeyAuthenticator(hostBasedMap));
+                }
+                final var publicKeyMap = publicKeyMapBuilder.build();
+                if (!publicKeyMap.isEmpty()) {
+                    final var factory = new UserAuthPublicKeyFactory();
+                    factory.setSignatureFactories(server.getSignatureFactories());
+                    authFactoriesBuilder.add(factory);
+                    server.setPublickeyAuthenticator(new UserPublicKeyAuthenticator(publicKeyMap));
+                }
+                server.setUserAuthFactories(authFactoriesBuilder.build());
+            }
+        }
+    }
+}