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.transport.api.UnsupportedConfigurationException;
27 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.password.grouping.password.type.CleartextPassword;
28 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ClientIdentity;
29 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.Keepalives;
30 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ServerAuthentication;
31 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.common.rev230417.TransportParamsGrouping;
34 * Our internal-use {@link SshClient}. We reuse all the properties and logic of an {@link SshClient}, but we never allow
37 final class TransportSshClient extends SshClient {
38 private TransportSshClient() {
43 * Guaranteed to throw an exception.
45 * @throws UnsupportedOperationException always
48 @Deprecated(forRemoval = true)
49 @DoNotCall("Always throws UnsupportedOperationException")
51 throw new UnsupportedOperationException();
55 * Guaranteed to throw an exception.
57 * @throws UnsupportedOperationException always
60 @Deprecated(forRemoval = true)
61 @DoNotCall("Always throws UnsupportedOperationException")
63 throw new UnsupportedOperationException();
67 * A {@link ClientBuilder} producing {@link TransportSshClient}s. Also hosts adaptation from
68 * {@code ietf-netconf-client.yang} configuration.
70 static final class Builder extends ClientBuilder {
71 private final EventLoopGroup group;
73 private Keepalives keepAlives;
74 private ClientIdentity clientIdentity;
76 Builder(final EventLoopGroup group) {
77 this.group = requireNonNull(group);
80 Builder transportParams(final TransportParamsGrouping params) throws UnsupportedConfigurationException {
81 ConfigUtils.setTransportParams(this, params, TransportUtils::getClientKexFactories);
85 Builder keepAlives(final Keepalives newkeepAlives) {
86 keepAlives = newkeepAlives;
90 Builder clientIdentity(final ClientIdentity newClientIdentity) {
91 clientIdentity = newClientIdentity;
95 Builder serverAuthentication(final ServerAuthentication serverAuthentication)
96 throws UnsupportedConfigurationException {
97 final ServerKeyVerifier newVerifier;
98 if (serverAuthentication != null) {
99 final var certificatesList = ImmutableList.<Certificate>builder()
100 .addAll(ConfigUtils.extractCertificates(serverAuthentication.getCaCerts()))
101 .addAll(ConfigUtils.extractCertificates(serverAuthentication.getEeCerts()))
103 final var publicKeys = ConfigUtils.extractPublicKeys(serverAuthentication.getSshHostKeys());
104 if (certificatesList.isEmpty() && publicKeys.isEmpty()) {
105 throw new UnsupportedConfigurationException(
106 "Server authentication should contain either ssh-host-keys, or ca-certs, or ee-certs");
108 newVerifier = new ServerPublicKeyVerifier(certificatesList, publicKeys);
113 serverKeyVerifier(newVerifier);
117 TransportSshClient buildChecked() throws UnsupportedConfigurationException {
118 final var ret = (TransportSshClient) super.build(true);
119 if (keepAlives != null) {
120 ConfigUtils.setKeepAlives(ret, keepAlives.getMaxWait(), keepAlives.getMaxAttempts());
122 ConfigUtils.setKeepAlives(ret, null, null);
124 if (clientIdentity != null && clientIdentity.getNone() == null) {
125 setClientIdentity(ret, clientIdentity);
127 ret.setScheduledExecutorService(group);
131 } catch (IllegalArgumentException e) {
132 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
138 * Guaranteed to throw an exception.
140 * @throws UnsupportedOperationException always
143 @Deprecated(forRemoval = true)
144 @DoNotCall("Always throws UnsupportedOperationException")
145 public TransportSshClient build() {
146 throw new UnsupportedOperationException();
150 * Guaranteed to throw an exception.
152 * @throws UnsupportedOperationException always
155 @Deprecated(forRemoval = true)
156 @DoNotCall("Always throws UnsupportedOperationException")
157 public TransportSshClient build(final boolean isFillWithDefaultValues) {
158 throw new UnsupportedOperationException();
162 protected ClientBuilder fillWithDefaultValues() {
163 if (factory == null) {
164 factory = TransportSshClient::new;
166 return super.fillWithDefaultValues();
169 private static void setClientIdentity(final TransportSshClient client, final ClientIdentity clientIdentity)
170 throws UnsupportedConfigurationException {
171 final var authFactoriesListBuilder = ImmutableList.<UserAuthFactory>builder();
172 final var password = clientIdentity.getPassword();
173 if (password != null) {
174 if (password.getPasswordType() instanceof CleartextPassword clearTextPassword) {
175 client.setPasswordIdentityProvider(
176 PasswordIdentityProvider.wrapPasswords(clearTextPassword.requireCleartextPassword()));
177 authFactoriesListBuilder.add(new UserAuthPasswordFactory());
179 // TODO support encrypted password -- requires augmentation of default schema
181 final var hostBased = clientIdentity.getHostbased();
182 if (hostBased != null) {
183 var keyPair = ConfigUtils.extractKeyPair(hostBased.getInlineOrKeystore());
184 var factory = new UserAuthHostBasedFactory();
185 factory.setClientHostKeys(HostKeyIdentityProvider.wrap(keyPair));
186 factory.setClientUsername(clientIdentity.getUsername());
187 factory.setClientHostname(null); // not provided via config
188 factory.setSignatureFactories(client.getSignatureFactories());
189 authFactoriesListBuilder.add(factory);
191 final var publicKey = clientIdentity.getPublicKey();
192 if (publicKey != null) {
193 final var keyPairs = ConfigUtils.extractKeyPair(publicKey.getInlineOrKeystore());
194 client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
195 final var factory = new UserAuthPublicKeyFactory();
196 factory.setSignatureFactories(client.getSignatureFactories());
197 authFactoriesListBuilder.add(factory);
199 // FIXME implement authentication using X509 certificate
200 final var userAuthFactories = authFactoriesListBuilder.build();
201 if (userAuthFactories.isEmpty()) {
202 throw new UnsupportedConfigurationException("Client Identity has no authentication mechanism defined");
204 client.setUserAuthFactories(userAuthFactories);