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 com.google.common.collect.ImmutableList;
11 import com.google.errorprone.annotations.DoNotCall;
12 import java.security.cert.Certificate;
13 import org.opendaylight.netconf.shaded.sshd.client.ClientBuilder;
14 import org.opendaylight.netconf.shaded.sshd.client.SshClient;
15 import org.opendaylight.netconf.shaded.sshd.client.auth.UserAuthFactory;
16 import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.HostKeyIdentityProvider;
17 import org.opendaylight.netconf.shaded.sshd.client.auth.hostbased.UserAuthHostBasedFactory;
18 import org.opendaylight.netconf.shaded.sshd.client.auth.password.PasswordIdentityProvider;
19 import org.opendaylight.netconf.shaded.sshd.client.auth.password.UserAuthPasswordFactory;
20 import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
21 import org.opendaylight.netconf.shaded.sshd.client.keyverifier.ServerKeyVerifier;
22 import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyIdentityProvider;
23 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
24 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.password.grouping.password.type.CleartextPassword;
25 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ClientIdentity;
26 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.Keepalives;
27 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ServerAuthentication;
28 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.common.rev230417.TransportParamsGrouping;
31 * Our internal-use {@link SshClient}. We reuse all the properties and logic of an {@link SshClient}, but we never allow
34 final class TransportSshClient extends SshClient {
35 private TransportSshClient() {
40 * Guaranteed to throw an exception.
42 * @throws UnsupportedOperationException always
45 @Deprecated(forRemoval = true)
46 @DoNotCall("Always throws UnsupportedOperationException")
48 throw new UnsupportedOperationException();
52 * Guaranteed to throw an exception.
54 * @throws UnsupportedOperationException always
57 @Deprecated(forRemoval = true)
58 @DoNotCall("Always throws UnsupportedOperationException")
60 throw new UnsupportedOperationException();
64 * A {@link ClientBuilder} producing {@link TransportSshClient}s. Also hosts adaptation from
65 * {@code ietf-netconf-client.yang} configuration.
67 static final class Builder extends ClientBuilder {
68 private Keepalives keepAlives;
69 private ClientIdentity clientIdentity;
71 Builder transportParams(final TransportParamsGrouping params) throws UnsupportedConfigurationException {
72 ConfigUtils.setTransportParams(this, params, TransportUtils::getClientKexFactories);
76 Builder keepAlives(final Keepalives newkeepAlives) {
77 keepAlives = newkeepAlives;
81 Builder clientIdentity(final ClientIdentity newClientIdentity) {
82 clientIdentity = newClientIdentity;
86 Builder serverAuthentication(final ServerAuthentication serverAuthentication)
87 throws UnsupportedConfigurationException {
88 final ServerKeyVerifier newVerifier;
89 if (serverAuthentication != null) {
90 final var certificatesList = ImmutableList.<Certificate>builder()
91 .addAll(ConfigUtils.extractCertificates(serverAuthentication.getCaCerts()))
92 .addAll(ConfigUtils.extractCertificates(serverAuthentication.getEeCerts()))
94 final var publicKeys = ConfigUtils.extractPublicKeys(serverAuthentication.getSshHostKeys());
95 if (certificatesList.isEmpty() && publicKeys.isEmpty()) {
96 throw new UnsupportedConfigurationException(
97 "Server authentication should contain either ssh-host-keys, or ca-certs, or ee-certs");
99 newVerifier = new ServerPublicKeyVerifier(certificatesList, publicKeys);
104 serverKeyVerifier(newVerifier);
108 TransportSshClient buildChecked() throws UnsupportedConfigurationException {
109 final var ret = (TransportSshClient) super.build(true);
110 if (keepAlives != null) {
111 ConfigUtils.setKeepAlives(ret, keepAlives.getMaxWait(), keepAlives.getMaxAttempts());
113 ConfigUtils.setKeepAlives(ret, null, null);
115 if (clientIdentity != null && clientIdentity.getNone() == null) {
116 setClientIdentity(ret, clientIdentity);
119 // FIXME: this is the default added by checkConfig(), but we really want to use an EventLoopGroup for this
120 // ret.setScheduledExecutorService(group);
124 } catch (IllegalArgumentException e) {
125 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
131 * Guaranteed to throw an exception.
133 * @throws UnsupportedOperationException always
136 @Deprecated(forRemoval = true)
137 @DoNotCall("Always throws UnsupportedOperationException")
138 public TransportSshClient build() {
139 throw new UnsupportedOperationException();
143 * Guaranteed to throw an exception.
145 * @throws UnsupportedOperationException always
148 @Deprecated(forRemoval = true)
149 @DoNotCall("Always throws UnsupportedOperationException")
150 public TransportSshClient build(final boolean isFillWithDefaultValues) {
151 throw new UnsupportedOperationException();
155 protected ClientBuilder fillWithDefaultValues() {
156 if (factory == null) {
157 factory = TransportSshClient::new;
159 return super.fillWithDefaultValues();
162 private static void setClientIdentity(final TransportSshClient client, final ClientIdentity clientIdentity)
163 throws UnsupportedConfigurationException {
164 final var authFactoriesListBuilder = ImmutableList.<UserAuthFactory>builder();
165 final var password = clientIdentity.getPassword();
166 if (password != null) {
167 if (password.getPasswordType() instanceof CleartextPassword clearTextPassword) {
168 client.setPasswordIdentityProvider(
169 PasswordIdentityProvider.wrapPasswords(clearTextPassword.requireCleartextPassword()));
170 authFactoriesListBuilder.add(new UserAuthPasswordFactory());
172 // TODO support encrypted password -- requires augmentation of default schema
174 final var hostBased = clientIdentity.getHostbased();
175 if (hostBased != null) {
176 var keyPair = ConfigUtils.extractKeyPair(hostBased.getInlineOrKeystore());
177 var factory = new UserAuthHostBasedFactory();
178 factory.setClientHostKeys(HostKeyIdentityProvider.wrap(keyPair));
179 factory.setClientUsername(clientIdentity.getUsername());
180 factory.setClientHostname(null); // not provided via config
181 factory.setSignatureFactories(client.getSignatureFactories());
182 authFactoriesListBuilder.add(factory);
184 final var publicKey = clientIdentity.getPublicKey();
185 if (publicKey != null) {
186 final var keyPairs = ConfigUtils.extractKeyPair(publicKey.getInlineOrKeystore());
187 client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
188 final var factory = new UserAuthPublicKeyFactory();
189 factory.setSignatureFactories(client.getSignatureFactories());
190 authFactoriesListBuilder.add(factory);
192 // FIXME implement authentication using X509 certificate
193 final var userAuthFactories = authFactoriesListBuilder.build();
194 if (userAuthFactories.isEmpty()) {
195 throw new UnsupportedConfigurationException("Client Identity has no authentication mechanism defined");
197 client.setUserAuthFactories(userAuthFactories);