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.topology.spi;
10 import static java.util.Objects.requireNonNull;
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.security.KeyPair;
17 import java.security.Provider;
18 import java.security.Security;
19 import java.util.Base64;
20 import java.util.List;
21 import javax.inject.Inject;
22 import javax.inject.Singleton;
23 import org.bouncycastle.jce.provider.BouncyCastleProvider;
24 import org.bouncycastle.openssl.PEMEncryptedKeyPair;
25 import org.bouncycastle.openssl.PEMKeyPair;
26 import org.bouncycastle.openssl.PEMParser;
27 import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
28 import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
29 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
30 import org.opendaylight.netconf.client.conf.NetconfClientConfiguration.NetconfClientProtocol;
31 import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder;
32 import org.opendaylight.netconf.client.mdsal.api.CredentialProvider;
33 import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider;
34 import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
35 import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyIdentityProvider;
36 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.password.grouping.password.type.CleartextPasswordBuilder;
37 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;
38 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;
39 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.ClientIdentity;
40 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.ClientIdentityBuilder;
41 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.client.identity.PasswordBuilder;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.Protocol.Name;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.Credentials;
44 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.KeyAuth;
45 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.LoginPw;
46 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.LoginPwUnencrypted;
47 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev231121.NetconfNode;
48 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
49 import org.osgi.service.component.annotations.Activate;
50 import org.osgi.service.component.annotations.Component;
51 import org.osgi.service.component.annotations.Reference;
54 * Default implementation of NetconfClientConfigurationBuildFactory.
58 public final class NetconfClientConfigurationBuilderFactoryImpl implements NetconfClientConfigurationBuilderFactory {
59 private static final Provider BCPROV;
62 final var prov = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
63 BCPROV = prov != null ? prov : new BouncyCastleProvider();
66 private final SslHandlerFactoryProvider sslHandlerFactoryProvider;
67 private final AAAEncryptionService encryptionService;
68 private final CredentialProvider credentialProvider;
72 public NetconfClientConfigurationBuilderFactoryImpl(
73 @Reference final AAAEncryptionService encryptionService,
74 @Reference final CredentialProvider credentialProvider,
75 @Reference final SslHandlerFactoryProvider sslHandlerFactoryProvider) {
76 this.encryptionService = requireNonNull(encryptionService);
77 this.credentialProvider = requireNonNull(credentialProvider);
78 this.sslHandlerFactoryProvider = requireNonNull(sslHandlerFactoryProvider);
82 public NetconfClientConfigurationBuilder createClientConfigurationBuilder(final NodeId nodeId,
83 final NetconfNode node) {
84 final var builder = NetconfClientConfigurationBuilder.create();
85 final var protocol = node.getProtocol();
86 if (node.requireTcpOnly()) {
87 builder.withProtocol(NetconfClientProtocol.TCP);
88 } else if (protocol == null || protocol.getName() == Name.SSH) {
89 builder.withProtocol(NetconfClientProtocol.SSH);
90 setSshParametersFromCredentials(builder, node.getCredentials());
91 } else if (protocol.getName() == Name.TLS) {
92 builder.withProtocol(NetconfClientProtocol.TLS).withSslHandlerFactory(
93 channel -> sslHandlerFactoryProvider.getSslHandlerFactory(protocol.getSpecification())
96 throw new IllegalArgumentException("Unsupported protocol type: " + protocol.getName());
99 final var helloCapabilities = node.getOdlHelloMessageCapabilities();
100 if (helloCapabilities != null) {
101 builder.withOdlHelloCapabilities(List.copyOf(helloCapabilities.requireCapability()));
105 .withName(nodeId.getValue())
106 .withTcpParameters(new TcpClientParametersBuilder()
107 .setRemoteAddress(node.requireHost())
108 .setRemotePort(node.requirePort()).build())
109 .withConnectionTimeoutMillis(node.requireConnectionTimeoutMillis().toJava());
112 private void setSshParametersFromCredentials(final NetconfClientConfigurationBuilder confBuilder,
113 final Credentials credentials) {
114 final var sshParamsBuilder = new SshClientParametersBuilder();
115 if (credentials instanceof LoginPwUnencrypted unencrypted) {
116 final var loginPassword = unencrypted.getLoginPasswordUnencrypted();
117 sshParamsBuilder.setClientIdentity(loginPasswordIdentity(
118 loginPassword.getUsername(), loginPassword.getPassword()));
119 } else if (credentials instanceof LoginPw loginPw) {
120 final var loginPassword = loginPw.getLoginPassword();
121 final var username = loginPassword.getUsername();
122 final var password = Base64.getEncoder().encodeToString(loginPassword.getPassword());
123 sshParamsBuilder.setClientIdentity(loginPasswordIdentity(username, encryptionService.decrypt(password)));
124 } else if (credentials instanceof KeyAuth keyAuth) {
125 final var keyBased = keyAuth.getKeyBased();
126 sshParamsBuilder.setClientIdentity(new ClientIdentityBuilder().setUsername(keyBased.getUsername()).build());
127 confBuilder.withSshConfigurator(factoryMgr -> {
128 final var keyPair = getKeyPair(keyBased.getKeyId());
129 factoryMgr.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPair));
130 final var factory = new UserAuthPublicKeyFactory();
131 factory.setSignatureFactories(factoryMgr.getSignatureFactories());
132 factoryMgr.setUserAuthFactories(List.of(factory));
135 throw new IllegalArgumentException("Unsupported credential type: " + credentials.getClass());
137 confBuilder.withSshParameters(sshParamsBuilder.build());
140 private static ClientIdentity loginPasswordIdentity(final String username, final String password) {
141 return new ClientIdentityBuilder()
142 .setUsername(requireNonNull(username, "username is undefined"))
143 .setPassword(new PasswordBuilder()
144 .setPasswordType(new CleartextPasswordBuilder()
145 .setCleartextPassword(requireNonNull(password, "password is undefined"))
151 private KeyPair getKeyPair(final String keyId) {
152 // public key retrieval logic taken from DatastoreBackedPublicKeyAuth
153 final var dsKeypair = credentialProvider.credentialForId(keyId);
154 if (dsKeypair == null) {
155 throw new IllegalArgumentException("No keypair found with keyId=" + keyId);
157 final var passPhrase = Strings.isNullOrEmpty(dsKeypair.getPassphrase()) ? "" : dsKeypair.getPassphrase();
159 return decodePrivateKey(encryptionService.decrypt(dsKeypair.getPrivateKey()),
160 encryptionService.decrypt(passPhrase));
161 } catch (IOException e) {
162 throw new IllegalStateException("Could not decode private key with keyId=" + keyId, e);
167 static KeyPair decodePrivateKey(final String privateKey, final String passphrase) throws IOException {
168 try (var keyReader = new PEMParser(new StringReader(privateKey.replace("\\n", "\n")))) {
169 final var obj = keyReader.readObject();
171 final PEMKeyPair keyPair;
172 if (obj instanceof PEMEncryptedKeyPair encrypted) {
173 keyPair = encrypted.decryptKeyPair(new JcePEMDecryptorProviderBuilder()
175 .build(passphrase.toCharArray()));
176 } else if (obj instanceof PEMKeyPair plain) {
179 throw new IllegalArgumentException("Unhandled private key " + obj.getClass());
182 return new JcaPEMKeyConverter().getKeyPair(keyPair);