Introduce TransportSsh{Client,Server}
[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 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;
29
30 /**
31  * Our internal-use {@link SshClient}. We reuse all the properties and logic of an {@link SshClient}, but we never allow
32  * it to be started.
33  */
34 final class TransportSshClient extends SshClient {
35     private TransportSshClient() {
36         // Hidden on purpose
37     }
38
39     /**
40      * Guaranteed to throw an exception.
41      *
42      * @throws UnsupportedOperationException always
43      */
44     @Override
45     @Deprecated(forRemoval = true)
46     @DoNotCall("Always throws UnsupportedOperationException")
47     public void start() {
48         throw new UnsupportedOperationException();
49     }
50
51     /**
52      * Guaranteed to throw an exception.
53      *
54      * @throws UnsupportedOperationException always
55      */
56     @Override
57     @Deprecated(forRemoval = true)
58     @DoNotCall("Always throws UnsupportedOperationException")
59     public void stop() {
60         throw new UnsupportedOperationException();
61     }
62
63     /**
64      * A {@link ClientBuilder} producing {@link TransportSshClient}s. Also hosts adaptation from
65      * {@code ietf-netconf-client.yang} configuration.
66      */
67     static final class Builder extends ClientBuilder {
68         private Keepalives keepAlives;
69         private ClientIdentity clientIdentity;
70
71         Builder transportParams(final TransportParamsGrouping params) throws UnsupportedConfigurationException {
72             ConfigUtils.setTransportParams(this, params, TransportUtils::getClientKexFactories);
73             return this;
74         }
75
76         Builder keepAlives(final Keepalives newkeepAlives) {
77             keepAlives = newkeepAlives;
78             return this;
79         }
80
81         Builder clientIdentity(final ClientIdentity newClientIdentity) {
82             clientIdentity = newClientIdentity;
83             return this;
84         }
85
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()))
93                     .build();
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");
98                 }
99                 newVerifier = new ServerPublicKeyVerifier(certificatesList, publicKeys);
100             } else {
101                 newVerifier = null;
102             }
103
104             serverKeyVerifier(newVerifier);
105             return this;
106         }
107
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());
112             } else {
113                 ConfigUtils.setKeepAlives(ret, null, null);
114             }
115             if (clientIdentity != null && clientIdentity.getNone() == null) {
116                 setClientIdentity(ret, clientIdentity);
117             }
118
119             // FIXME: this is the default added by checkConfig(), but we really want to use an EventLoopGroup for this
120             // ret.setScheduledExecutorService(group);
121
122             try {
123                 ret.checkConfig();
124             } catch (IllegalArgumentException e) {
125                 throw new UnsupportedConfigurationException("Inconsistent client configuration", e);
126             }
127             return ret;
128         }
129
130         /**
131          * Guaranteed to throw an exception.
132          *
133          * @throws UnsupportedOperationException always
134          */
135         @Override
136         @Deprecated(forRemoval = true)
137         @DoNotCall("Always throws UnsupportedOperationException")
138         public TransportSshClient build() {
139             throw new UnsupportedOperationException();
140         }
141
142         /**
143          * Guaranteed to throw an exception.
144          *
145          * @throws UnsupportedOperationException always
146          */
147         @Override
148         @Deprecated(forRemoval = true)
149         @DoNotCall("Always throws UnsupportedOperationException")
150         public TransportSshClient build(final boolean isFillWithDefaultValues) {
151             throw new UnsupportedOperationException();
152         }
153
154         @Override
155         protected ClientBuilder fillWithDefaultValues() {
156             if (factory == null) {
157                 factory = TransportSshClient::new;
158             }
159             return super.fillWithDefaultValues();
160         }
161
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());
171                 }
172                 // TODO support encrypted password -- requires augmentation of default schema
173             }
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);
183             }
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);
191             }
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");
196             }
197             client.setUserAuthFactories(userAuthFactories);
198         }
199     }
200 }