9b680f8cb9e52c2e67b681ab9d9a46676cf14276
[netconf.git] / transport / transport-ssh / src / main / java / org / opendaylight / netconf / transport / ssh / TransportSshClient.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.netconf.transport.ssh;
9
10 import static java.util.Objects.requireNonNull;
11
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;
32
33 /**
34  * Our internal-use {@link SshClient}. We reuse all the properties and logic of an {@link SshClient}, but we never allow
35  * it to be started.
36  */
37 final class TransportSshClient extends SshClient {
38     private TransportSshClient() {
39         // Hidden on purpose
40     }
41
42     /**
43      * Guaranteed to throw an exception.
44      *
45      * @throws UnsupportedOperationException always
46      */
47     @Override
48     @Deprecated(forRemoval = true)
49     @DoNotCall("Always throws UnsupportedOperationException")
50     public void start() {
51         throw new UnsupportedOperationException();
52     }
53
54     /**
55      * Guaranteed to throw an exception.
56      *
57      * @throws UnsupportedOperationException always
58      */
59     @Override
60     @Deprecated(forRemoval = true)
61     @DoNotCall("Always throws UnsupportedOperationException")
62     public void stop() {
63         throw new UnsupportedOperationException();
64     }
65
66     /**
67      * A {@link ClientBuilder} producing {@link TransportSshClient}s. Also hosts adaptation from
68      * {@code ietf-netconf-client.yang} configuration.
69      */
70     static final class Builder extends ClientBuilder {
71         private final EventLoopGroup group;
72
73         private Keepalives keepAlives;
74         private ClientIdentity clientIdentity;
75
76         Builder(final EventLoopGroup group) {
77             this.group = requireNonNull(group);
78         }
79
80         Builder transportParams(final TransportParamsGrouping params) throws UnsupportedConfigurationException {
81             ConfigUtils.setTransportParams(this, params, TransportUtils::getClientKexFactories);
82             return this;
83         }
84
85         Builder keepAlives(final Keepalives newkeepAlives) {
86             keepAlives = newkeepAlives;
87             return this;
88         }
89
90         Builder clientIdentity(final ClientIdentity newClientIdentity) {
91             clientIdentity = newClientIdentity;
92             return this;
93         }
94
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()))
102                     .build();
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");
107                 }
108                 newVerifier = new ServerPublicKeyVerifier(certificatesList, publicKeys);
109             } else {
110                 newVerifier = null;
111             }
112
113             serverKeyVerifier(newVerifier);
114             return this;
115         }
116
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());
121             } else {
122                 ConfigUtils.setKeepAlives(ret, null, null);
123             }
124             if (clientIdentity != null && clientIdentity.getNone() == null) {
125                 setClientIdentity(ret, clientIdentity);
126             }
127             ret.setScheduledExecutorService(group);
128
129             try {
130                 ret.checkConfig();
131             } catch (IllegalArgumentException e) {
132                 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
133             }
134             return ret;
135         }
136
137         /**
138          * Guaranteed to throw an exception.
139          *
140          * @throws UnsupportedOperationException always
141          */
142         @Override
143         @Deprecated(forRemoval = true)
144         @DoNotCall("Always throws UnsupportedOperationException")
145         public TransportSshClient build() {
146             throw new UnsupportedOperationException();
147         }
148
149         /**
150          * Guaranteed to throw an exception.
151          *
152          * @throws UnsupportedOperationException always
153          */
154         @Override
155         @Deprecated(forRemoval = true)
156         @DoNotCall("Always throws UnsupportedOperationException")
157         public TransportSshClient build(final boolean isFillWithDefaultValues) {
158             throw new UnsupportedOperationException();
159         }
160
161         @Override
162         protected ClientBuilder fillWithDefaultValues() {
163             if (factory == null) {
164                 factory = TransportSshClient::new;
165             }
166             return super.fillWithDefaultValues();
167         }
168
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());
178                 }
179                 // TODO support encrypted password -- requires augmentation of default schema
180             }
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);
190             }
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);
198             }
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");
203             }
204             client.setUserAuthFactories(userAuthFactories);
205         }
206     }
207 }