Fix SslHandler indirection
[netconf.git] / apps / netconf-topology / src / main / java / org / opendaylight / netconf / topology / spi / NetconfClientConfigurationBuilderFactoryImpl.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.topology.spi;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.VisibleForTesting;
13 import com.google.common.base.Strings;
14 import java.io.IOException;
15 import java.io.StringReader;
16 import java.nio.charset.StandardCharsets;
17 import java.security.GeneralSecurityException;
18 import java.security.KeyPair;
19 import java.security.Provider;
20 import java.security.Security;
21 import java.util.Base64;
22 import java.util.List;
23 import javax.inject.Inject;
24 import javax.inject.Singleton;
25 import org.bouncycastle.jce.provider.BouncyCastleProvider;
26 import org.bouncycastle.openssl.PEMEncryptedKeyPair;
27 import org.bouncycastle.openssl.PEMKeyPair;
28 import org.bouncycastle.openssl.PEMParser;
29 import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
30 import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
31 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
32 import org.opendaylight.netconf.client.conf.NetconfClientConfiguration.NetconfClientProtocol;
33 import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder;
34 import org.opendaylight.netconf.client.mdsal.api.CredentialProvider;
35 import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider;
36 import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
37 import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyIdentityProvider;
38 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.password.grouping.password.type.CleartextPasswordBuilder;
39 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev231228.netconf.client.initiate.stack.grouping.transport.ssh.ssh.SshClientParametersBuilder;
40 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev231228.netconf.client.initiate.stack.grouping.transport.ssh.ssh.TcpClientParametersBuilder;
41 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.ClientIdentity;
42 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.ClientIdentityBuilder;
43 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.client.identity.PasswordBuilder;
44 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.Protocol.Name;
45 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.Credentials;
46 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.KeyAuth;
47 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.LoginPw;
48 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.LoginPwUnencrypted;
49 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev231121.NetconfNode;
50 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
51 import org.osgi.service.component.annotations.Activate;
52 import org.osgi.service.component.annotations.Component;
53 import org.osgi.service.component.annotations.Reference;
54
55 /**
56  * Default implementation of NetconfClientConfigurationBuildFactory.
57  */
58 @Component
59 @Singleton
60 public final class NetconfClientConfigurationBuilderFactoryImpl implements NetconfClientConfigurationBuilderFactory {
61     private static final Provider BCPROV;
62
63     static {
64         final var prov = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
65         BCPROV = prov != null ? prov : new BouncyCastleProvider();
66     }
67
68     private final SslHandlerFactoryProvider sslHandlerFactoryProvider;
69     private final AAAEncryptionService encryptionService;
70     private final CredentialProvider credentialProvider;
71
72     @Inject
73     @Activate
74     public NetconfClientConfigurationBuilderFactoryImpl(
75             @Reference final AAAEncryptionService encryptionService,
76             @Reference final CredentialProvider credentialProvider,
77             @Reference final SslHandlerFactoryProvider sslHandlerFactoryProvider) {
78         this.encryptionService = requireNonNull(encryptionService);
79         this.credentialProvider = requireNonNull(credentialProvider);
80         this.sslHandlerFactoryProvider = requireNonNull(sslHandlerFactoryProvider);
81     }
82
83     @Override
84     public NetconfClientConfigurationBuilder createClientConfigurationBuilder(final NodeId nodeId,
85         final NetconfNode node) {
86         final var builder = NetconfClientConfigurationBuilder.create();
87         final var protocol = node.getProtocol();
88         if (node.requireTcpOnly()) {
89             builder.withProtocol(NetconfClientProtocol.TCP);
90         } else if (protocol == null || protocol.getName() == Name.SSH) {
91             builder.withProtocol(NetconfClientProtocol.SSH);
92             setSshParametersFromCredentials(builder, node.getCredentials());
93         } else if (protocol.getName() == Name.TLS) {
94             final var handlerFactory = sslHandlerFactoryProvider.getSslHandlerFactory(protocol.getSpecification());
95             builder.withProtocol(NetconfClientProtocol.TLS)
96                 .withSslHandlerFactory(channel -> handlerFactory.createSslHandler());
97         } else {
98             throw new IllegalArgumentException("Unsupported protocol type: " + protocol.getName());
99         }
100
101         final var helloCapabilities = node.getOdlHelloMessageCapabilities();
102         if (helloCapabilities != null) {
103             builder.withOdlHelloCapabilities(List.copyOf(helloCapabilities.requireCapability()));
104         }
105
106         return builder
107             .withName(nodeId.getValue())
108             .withTcpParameters(new TcpClientParametersBuilder()
109                 .setRemoteAddress(node.requireHost())
110                 .setRemotePort(node.requirePort()).build())
111             .withConnectionTimeoutMillis(node.requireConnectionTimeoutMillis().toJava());
112     }
113
114     private void setSshParametersFromCredentials(final NetconfClientConfigurationBuilder confBuilder,
115             final Credentials credentials) {
116         final var sshParamsBuilder = new SshClientParametersBuilder();
117         if (credentials instanceof LoginPwUnencrypted unencrypted) {
118             final var loginPassword = unencrypted.getLoginPasswordUnencrypted();
119             sshParamsBuilder.setClientIdentity(loginPasswordIdentity(
120                 loginPassword.getUsername(), loginPassword.getPassword()));
121         } else if (credentials instanceof LoginPw loginPw) {
122             final var loginPassword = loginPw.getLoginPassword();
123             final var username = loginPassword.getUsername();
124
125             final byte[] plainBytes;
126             try {
127                 plainBytes = encryptionService.decrypt(loginPassword.getPassword());
128             } catch (GeneralSecurityException e) {
129                 throw new IllegalStateException("Failed to decrypt password", e);
130             }
131
132             sshParamsBuilder.setClientIdentity(loginPasswordIdentity(username,
133                 new String(plainBytes, StandardCharsets.UTF_8)));
134         } else if (credentials instanceof KeyAuth keyAuth) {
135             final var keyBased = keyAuth.getKeyBased();
136             sshParamsBuilder.setClientIdentity(new ClientIdentityBuilder().setUsername(keyBased.getUsername()).build());
137             confBuilder.withSshConfigurator(factoryMgr -> {
138                 final var keyPair = getKeyPair(keyBased.getKeyId());
139                 factoryMgr.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPair));
140                 final var factory = new UserAuthPublicKeyFactory();
141                 factory.setSignatureFactories(factoryMgr.getSignatureFactories());
142                 factoryMgr.setUserAuthFactories(List.of(factory));
143             });
144         } else {
145             throw new IllegalArgumentException("Unsupported credential type: " + credentials.getClass());
146         }
147         confBuilder.withSshParameters(sshParamsBuilder.build());
148     }
149
150     private static ClientIdentity loginPasswordIdentity(final String username, final String password) {
151         return new ClientIdentityBuilder()
152             .setUsername(requireNonNull(username, "username is undefined"))
153             .setPassword(new PasswordBuilder()
154                 .setPasswordType(new CleartextPasswordBuilder()
155                     .setCleartextPassword(requireNonNull(password, "password is undefined"))
156                     .build())
157                 .build())
158             .build();
159     }
160
161     private KeyPair getKeyPair(final String keyId) {
162         // public key retrieval logic taken from DatastoreBackedPublicKeyAuth
163         final var dsKeypair = credentialProvider.credentialForId(keyId);
164         if (dsKeypair == null) {
165             throw new IllegalArgumentException("No keypair found with keyId=" + keyId);
166         }
167         final var passPhrase = Strings.isNullOrEmpty(dsKeypair.getPassphrase()) ? "" : dsKeypair.getPassphrase();
168         try {
169             return decodePrivateKey(decryptString(dsKeypair.getPrivateKey()), decryptString(passPhrase));
170         } catch (IOException e) {
171             throw new IllegalStateException("Could not decode private key with keyId=" + keyId, e);
172         }
173     }
174
175     private String decryptString(final String encrypted) {
176         final byte[] cryptobytes = Base64.getDecoder().decode(encrypted);
177         final byte[] clearbytes;
178         try {
179             clearbytes = encryptionService.decrypt(cryptobytes);
180         } catch (GeneralSecurityException e) {
181             throw new IllegalStateException("Failed to decrypt", e);
182         }
183         return new String(clearbytes, StandardCharsets.UTF_8);
184     }
185
186
187     @VisibleForTesting
188     static KeyPair decodePrivateKey(final String privateKey, final String passphrase) throws IOException {
189         try (var keyReader = new PEMParser(new StringReader(privateKey.replace("\\n", "\n")))) {
190             final var obj = keyReader.readObject();
191
192             final PEMKeyPair keyPair;
193             if (obj instanceof PEMEncryptedKeyPair encrypted) {
194                 keyPair = encrypted.decryptKeyPair(new JcePEMDecryptorProviderBuilder()
195                     .setProvider(BCPROV)
196                     .build(passphrase.toCharArray()));
197             } else if (obj instanceof PEMKeyPair plain) {
198                 keyPair = plain;
199             } else {
200                 throw new IllegalArgumentException("Unhandled private key " + obj.getClass());
201             }
202
203             return new JcaPEMKeyConverter().getKeyPair(keyPair);
204         }
205     }
206 }