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 io.netty.channel.EventLoopGroup;
15 import java.security.cert.Certificate;
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.rev230417.password.grouping.password.type.CleartextPassword;
29 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ClientIdentity;
30 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.Keepalives;
31 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ServerAuthentication;
32 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.common.rev230417.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 EventLoopGroup group;
75 private Keepalives keepAlives;
76 private ClientIdentity clientIdentity;
78 Builder(final NettyIoServiceFactoryFactory ioServiceFactory, final EventLoopGroup group) {
79 this.ioServiceFactory = requireNonNull(ioServiceFactory);
80 this.group = requireNonNull(group);
83 Builder transportParams(final TransportParamsGrouping params) throws UnsupportedConfigurationException {
84 ConfigUtils.setTransportParams(this, params, TransportUtils::getClientKexFactories);
88 Builder keepAlives(final Keepalives newkeepAlives) {
89 keepAlives = newkeepAlives;
93 Builder clientIdentity(final ClientIdentity newClientIdentity) {
94 clientIdentity = newClientIdentity;
98 Builder serverAuthentication(final ServerAuthentication serverAuthentication)
99 throws UnsupportedConfigurationException {
100 final ServerKeyVerifier newVerifier;
101 if (serverAuthentication != null) {
102 final var certificatesList = ImmutableList.<Certificate>builder()
103 .addAll(ConfigUtils.extractCertificates(serverAuthentication.getCaCerts()))
104 .addAll(ConfigUtils.extractCertificates(serverAuthentication.getEeCerts()))
106 final var publicKeys = ConfigUtils.extractPublicKeys(serverAuthentication.getSshHostKeys());
107 if (certificatesList.isEmpty() && publicKeys.isEmpty()) {
108 throw new UnsupportedConfigurationException(
109 "Server authentication should contain either ssh-host-keys, or ca-certs, or ee-certs");
111 newVerifier = new ServerPublicKeyVerifier(certificatesList, publicKeys);
116 serverKeyVerifier(newVerifier);
120 TransportSshClient buildChecked() throws UnsupportedConfigurationException {
121 final var ret = (TransportSshClient) super.build(true);
122 if (keepAlives != null) {
123 ConfigUtils.setKeepAlives(ret, keepAlives.getMaxWait(), keepAlives.getMaxAttempts());
125 ConfigUtils.setKeepAlives(ret, null, null);
127 if (clientIdentity == null) {
128 throw new UnsupportedConfigurationException("Client parameters are required");
130 final var username = clientIdentity.getUsername();
131 if (username == null) {
132 throw new UnsupportedConfigurationException("Client parameters are missing username");
135 if (clientIdentity != null && clientIdentity.getNone() == null) {
136 setClientIdentity(ret, clientIdentity);
138 ret.setIoServiceFactoryFactory(ioServiceFactory);
139 ret.setScheduledExecutorService(group);
143 } catch (IllegalArgumentException e) {
144 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
147 ret.setSessionFactory(new TransportClientSessionFactory(ret, username));
152 * Guaranteed to throw an exception.
154 * @throws UnsupportedOperationException always
157 @Deprecated(forRemoval = true)
158 @DoNotCall("Always throws UnsupportedOperationException")
159 public TransportSshClient build() {
160 throw new UnsupportedOperationException();
164 * Guaranteed to throw an exception.
166 * @throws UnsupportedOperationException always
169 @Deprecated(forRemoval = true)
170 @DoNotCall("Always throws UnsupportedOperationException")
171 public TransportSshClient build(final boolean isFillWithDefaultValues) {
172 throw new UnsupportedOperationException();
176 protected ClientBuilder fillWithDefaultValues() {
177 if (factory == null) {
178 factory = TransportSshClient::new;
180 return super.fillWithDefaultValues();
183 private static void setClientIdentity(final TransportSshClient client, final ClientIdentity clientIdentity)
184 throws UnsupportedConfigurationException {
185 final var authFactoriesListBuilder = ImmutableList.<UserAuthFactory>builder();
186 final var password = clientIdentity.getPassword();
187 if (password != null) {
188 if (password.getPasswordType() instanceof CleartextPassword clearTextPassword) {
189 client.setPasswordIdentityProvider(
190 PasswordIdentityProvider.wrapPasswords(clearTextPassword.requireCleartextPassword()));
191 authFactoriesListBuilder.add(new UserAuthPasswordFactory());
193 // TODO support encrypted password -- requires augmentation of default schema
195 final var hostBased = clientIdentity.getHostbased();
196 if (hostBased != null) {
197 var keyPair = ConfigUtils.extractKeyPair(hostBased.getInlineOrKeystore());
198 var factory = new UserAuthHostBasedFactory();
199 factory.setClientHostKeys(HostKeyIdentityProvider.wrap(keyPair));
200 factory.setClientUsername(clientIdentity.getUsername());
201 factory.setClientHostname(null); // not provided via config
202 factory.setSignatureFactories(client.getSignatureFactories());
203 authFactoriesListBuilder.add(factory);
205 final var publicKey = clientIdentity.getPublicKey();
206 if (publicKey != null) {
207 final var keyPairs = ConfigUtils.extractKeyPair(publicKey.getInlineOrKeystore());
208 client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
209 final var factory = new UserAuthPublicKeyFactory();
210 factory.setSignatureFactories(client.getSignatureFactories());
211 authFactoriesListBuilder.add(factory);
213 // FIXME implement authentication using X509 certificate
214 final var userAuthFactories = authFactoriesListBuilder.build();
215 if (userAuthFactories.isEmpty()) {
216 throw new UnsupportedConfigurationException("Client Identity has no authentication mechanism defined");
218 client.setUserAuthFactories(userAuthFactories);