2 * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.netconf.transport.ssh;
10 import static java.util.Objects.requireNonNull;
12 import com.google.common.collect.ImmutableList;
13 import com.google.errorprone.annotations.DoNotCall;
14 import java.security.cert.Certificate;
15 import java.util.concurrent.ScheduledExecutorService;
16 import org.opendaylight.netconf.shaded.sshd.client.ClientBuilder;
17 import org.opendaylight.netconf.shaded.sshd.client.SshClient;
18 import org.opendaylight.netconf.shaded.sshd.client.auth.UserAuthFactory;
19 import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.HostKeyIdentityProvider;
20 import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.UserAuthHostBasedFactory;
21 import org.opendaylight.netconf.shaded.sshd.client.auth.password.PasswordIdentityProvider;
22 import org.opendaylight.netconf.shaded.sshd.client.auth.password.UserAuthPasswordFactory;
23 import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
24 import org.opendaylight.netconf.shaded.sshd.client.keyverifier.ServerKeyVerifier;
25 import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyIdentityProvider;
26 import org.opendaylight.netconf.shaded.sshd.netty.NettyIoServiceFactoryFactory;
27 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
28 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.password.grouping.password.type.CleartextPassword;
29 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.ClientIdentity;
30 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.Keepalives;
31 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.ServerAuthentication;
32 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.common.rev231228.TransportParamsGrouping;
35 * Our internal-use {@link SshClient}. We reuse all the properties and logic of an {@link SshClient}, but we never allow
38 final class TransportSshClient extends SshClient {
39 private TransportSshClient() {
44 * Guaranteed to throw an exception.
46 * @throws UnsupportedOperationException always
49 @Deprecated(forRemoval = true)
50 @DoNotCall("Always throws UnsupportedOperationException")
52 throw new UnsupportedOperationException();
56 * Guaranteed to throw an exception.
58 * @throws UnsupportedOperationException always
61 @Deprecated(forRemoval = true)
62 @DoNotCall("Always throws UnsupportedOperationException")
64 throw new UnsupportedOperationException();
68 * A {@link ClientBuilder} producing {@link TransportSshClient}s. Also hosts adaptation from
69 * {@code ietf-netconf-client.yang} configuration.
71 static final class Builder extends ClientBuilder {
72 private final NettyIoServiceFactoryFactory ioServiceFactory;
73 private final ScheduledExecutorService executorService;
75 private ClientFactoryManagerConfigurator configurator;
76 private Keepalives keepAlives;
77 private ClientIdentity clientIdentity;
79 Builder(final NettyIoServiceFactoryFactory ioServiceFactory, final ScheduledExecutorService executorService) {
80 this.ioServiceFactory = requireNonNull(ioServiceFactory);
81 this.executorService = requireNonNull(executorService);
84 Builder transportParams(final TransportParamsGrouping params) throws UnsupportedConfigurationException {
85 ConfigUtils.setTransportParams(this, params, TransportUtils::getClientKexFactories);
89 Builder keepAlives(final Keepalives newKeepAlives) {
90 keepAlives = newKeepAlives;
94 Builder clientIdentity(final ClientIdentity newClientIdentity) {
95 clientIdentity = newClientIdentity;
99 Builder serverAuthentication(final ServerAuthentication serverAuthentication)
100 throws UnsupportedConfigurationException {
101 final ServerKeyVerifier newVerifier;
102 if (serverAuthentication != null) {
103 final var certificatesList = ImmutableList.<Certificate>builder()
104 .addAll(ConfigUtils.extractCertificates(serverAuthentication.getCaCerts()))
105 .addAll(ConfigUtils.extractCertificates(serverAuthentication.getEeCerts()))
107 final var publicKeys = ConfigUtils.extractPublicKeys(serverAuthentication.getSshHostKeys());
108 if (certificatesList.isEmpty() && publicKeys.isEmpty()) {
109 throw new UnsupportedConfigurationException(
110 "Server authentication should contain either ssh-host-keys, or ca-certs, or ee-certs");
112 newVerifier = new ServerPublicKeyVerifier(certificatesList, publicKeys);
117 serverKeyVerifier(newVerifier);
121 Builder configurator(final ClientFactoryManagerConfigurator newConfigurator) {
122 configurator = newConfigurator;
126 TransportSshClient buildChecked() throws UnsupportedConfigurationException {
127 final var ret = (TransportSshClient) super.build(true);
128 if (keepAlives != null) {
129 ConfigUtils.setKeepAlives(ret, keepAlives.getMaxWait(), keepAlives.getMaxAttempts());
131 ConfigUtils.setKeepAlives(ret, null, null);
133 if (clientIdentity == null) {
134 throw new UnsupportedConfigurationException("Client parameters are required");
136 final var username = clientIdentity.getUsername();
137 if (username == null) {
138 throw new UnsupportedConfigurationException("Client parameters are missing username");
141 if (clientIdentity != null && clientIdentity.getNone() == null) {
142 setClientIdentity(ret, clientIdentity, configurator == null);
144 if (configurator != null) {
145 configurator.configureClientFactoryManager(ret);
147 ret.setIoServiceFactoryFactory(ioServiceFactory);
148 ret.setScheduledExecutorService(executorService);
152 } catch (IllegalArgumentException e) {
153 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
156 ret.setSessionFactory(new TransportClientSessionFactory(ret, username));
161 * Guaranteed to throw an exception.
163 * @throws UnsupportedOperationException always
166 @Deprecated(forRemoval = true)
167 @DoNotCall("Always throws UnsupportedOperationException")
168 public TransportSshClient build() {
169 throw new UnsupportedOperationException();
173 * Guaranteed to throw an exception.
175 * @throws UnsupportedOperationException always
178 @Deprecated(forRemoval = true)
179 @DoNotCall("Always throws UnsupportedOperationException")
180 public TransportSshClient build(final boolean isFillWithDefaultValues) {
181 throw new UnsupportedOperationException();
185 protected ClientBuilder fillWithDefaultValues() {
186 if (factory == null) {
187 factory = TransportSshClient::new;
189 return super.fillWithDefaultValues();
192 private static void setClientIdentity(final TransportSshClient client, final ClientIdentity clientIdentity,
193 final boolean throwExceptionIfNoAuthMethodDefined) throws UnsupportedConfigurationException {
194 final var authFactoriesListBuilder = ImmutableList.<UserAuthFactory>builder();
195 final var password = clientIdentity.getPassword();
196 if (password != null) {
197 if (password.getPasswordType() instanceof CleartextPassword clearTextPassword) {
198 client.setPasswordIdentityProvider(
199 PasswordIdentityProvider.wrapPasswords(clearTextPassword.requireCleartextPassword()));
200 authFactoriesListBuilder.add(new UserAuthPasswordFactory());
202 // TODO support encrypted password -- requires augmentation of default schema
204 final var hostBased = clientIdentity.getHostbased();
205 if (hostBased != null) {
206 var keyPair = ConfigUtils.extractKeyPair(hostBased.getInlineOrKeystore());
207 var factory = new UserAuthHostBasedFactory();
208 factory.setClientHostKeys(HostKeyIdentityProvider.wrap(keyPair));
209 factory.setClientUsername(clientIdentity.getUsername());
210 // not provided via config
211 factory.setClientHostname(null);
212 factory.setSignatureFactories(client.getSignatureFactories());
213 authFactoriesListBuilder.add(factory);
215 final var publicKey = clientIdentity.getPublicKey();
216 if (publicKey != null) {
217 final var keyPairs = ConfigUtils.extractKeyPair(publicKey.getInlineOrKeystore());
218 client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
219 final var factory = new UserAuthPublicKeyFactory();
220 factory.setSignatureFactories(client.getSignatureFactories());
221 authFactoriesListBuilder.add(factory);
223 // FIXME implement authentication using X509 certificate
225 final var userAuthFactories = authFactoriesListBuilder.build();
226 if (!userAuthFactories.isEmpty()) {
227 client.setUserAuthFactories(userAuthFactories);
228 } else if (throwExceptionIfNoAuthMethodDefined) {
229 throw new UnsupportedConfigurationException("Client Identity has no authentication mechanism defined");