<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>
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;
// 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();
}
@FunctionalInterface
- private interface KexFactoryProvider {
+ interface KexFactoryProvider {
List<KeyExchangeFactory> getKexFactories(KeyExchange input) throws UnsupportedConfigurationException;
}
}
*/
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;
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
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;
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;
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);
}
/**
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
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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());
+ }
+ }
+ }
+}