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 && clientIdentity.getNone() == null) {
128 setClientIdentity(ret, clientIdentity);
130 ret.setIoServiceFactoryFactory(ioServiceFactory);
131 ret.setScheduledExecutorService(group);
135 } catch (IllegalArgumentException e) {
136 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
142 * Guaranteed to throw an exception.
144 * @throws UnsupportedOperationException always
147 @Deprecated(forRemoval = true)
148 @DoNotCall("Always throws UnsupportedOperationException")
149 public TransportSshClient build() {
150 throw new UnsupportedOperationException();
154 * Guaranteed to throw an exception.
156 * @throws UnsupportedOperationException always
159 @Deprecated(forRemoval = true)
160 @DoNotCall("Always throws UnsupportedOperationException")
161 public TransportSshClient build(final boolean isFillWithDefaultValues) {
162 throw new UnsupportedOperationException();
166 protected ClientBuilder fillWithDefaultValues() {
167 if (factory == null) {
168 factory = TransportSshClient::new;
170 return super.fillWithDefaultValues();
173 private static void setClientIdentity(final TransportSshClient client, final ClientIdentity clientIdentity)
174 throws UnsupportedConfigurationException {
175 final var authFactoriesListBuilder = ImmutableList.<UserAuthFactory>builder();
176 final var password = clientIdentity.getPassword();
177 if (password != null) {
178 if (password.getPasswordType() instanceof CleartextPassword clearTextPassword) {
179 client.setPasswordIdentityProvider(
180 PasswordIdentityProvider.wrapPasswords(clearTextPassword.requireCleartextPassword()));
181 authFactoriesListBuilder.add(new UserAuthPasswordFactory());
183 // TODO support encrypted password -- requires augmentation of default schema
185 final var hostBased = clientIdentity.getHostbased();
186 if (hostBased != null) {
187 var keyPair = ConfigUtils.extractKeyPair(hostBased.getInlineOrKeystore());
188 var factory = new UserAuthHostBasedFactory();
189 factory.setClientHostKeys(HostKeyIdentityProvider.wrap(keyPair));
190 factory.setClientUsername(clientIdentity.getUsername());
191 factory.setClientHostname(null); // not provided via config
192 factory.setSignatureFactories(client.getSignatureFactories());
193 authFactoriesListBuilder.add(factory);
195 final var publicKey = clientIdentity.getPublicKey();
196 if (publicKey != null) {
197 final var keyPairs = ConfigUtils.extractKeyPair(publicKey.getInlineOrKeystore());
198 client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
199 final var factory = new UserAuthPublicKeyFactory();
200 factory.setSignatureFactories(client.getSignatureFactories());
201 authFactoriesListBuilder.add(factory);
203 // FIXME implement authentication using X509 certificate
204 final var userAuthFactories = authFactoriesListBuilder.build();
205 if (userAuthFactories.isEmpty()) {
206 throw new UnsupportedConfigurationException("Client Identity has no authentication mechanism defined");
208 client.setUserAuthFactories(userAuthFactories);