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.client;
10 import static org.junit.jupiter.api.Assertions.assertNotNull;
11 import static org.mockito.ArgumentMatchers.any;
12 import static org.mockito.Mockito.doNothing;
13 import static org.mockito.Mockito.doReturn;
14 import static org.mockito.Mockito.timeout;
15 import static org.mockito.Mockito.verify;
17 import io.netty.handler.ssl.SslContextBuilder;
18 import java.math.BigInteger;
19 import java.net.InetAddress;
20 import java.net.ServerSocket;
21 import java.security.KeyPairGenerator;
22 import java.security.KeyStore;
23 import java.security.SecureRandom;
24 import java.security.cert.Certificate;
25 import java.security.spec.RSAKeyGenParameterSpec;
26 import java.time.Duration;
27 import java.time.Instant;
28 import java.util.Date;
29 import java.util.List;
31 import java.util.concurrent.TimeUnit;
32 import javax.net.ssl.KeyManagerFactory;
33 import javax.net.ssl.TrustManagerFactory;
34 import org.bouncycastle.asn1.x500.X500Name;
35 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
36 import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
37 import org.bouncycastle.jce.provider.BouncyCastleProvider;
38 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
39 import org.junit.jupiter.api.AfterAll;
40 import org.junit.jupiter.api.AfterEach;
41 import org.junit.jupiter.api.BeforeAll;
42 import org.junit.jupiter.api.BeforeEach;
43 import org.junit.jupiter.api.Test;
44 import org.junit.jupiter.api.extension.ExtendWith;
45 import org.mockito.Mock;
46 import org.mockito.junit.jupiter.MockitoExtension;
47 import org.opendaylight.netconf.client.conf.NetconfClientConfiguration;
48 import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder;
49 import org.opendaylight.netconf.shaded.sshd.client.auth.password.PasswordIdentityProvider;
50 import org.opendaylight.netconf.shaded.sshd.server.auth.password.UserAuthPasswordFactory;
51 import org.opendaylight.netconf.shaded.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
52 import org.opendaylight.netconf.transport.api.TransportChannel;
53 import org.opendaylight.netconf.transport.api.TransportChannelListener;
54 import org.opendaylight.netconf.transport.ssh.ClientFactoryManagerConfigurator;
55 import org.opendaylight.netconf.transport.ssh.SSHTransportStackFactory;
56 import org.opendaylight.netconf.transport.ssh.ServerFactoryManagerConfigurator;
57 import org.opendaylight.netconf.transport.tcp.TCPServer;
58 import org.opendaylight.netconf.transport.tls.TLSServer;
59 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.crypt.hash.rev140806.CryptHash;
60 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.RsaPrivateKeyFormat;
61 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.SubjectPublicKeyInfoFormat;
62 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228._private.key.grouping._private.key.type.CleartextPrivateKeyBuilder;
63 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.password.grouping.password.type.CleartextPasswordBuilder;
64 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
65 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
66 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
67 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228.inline.or.keystore.asymmetric.key.grouping.inline.or.keystore.InlineBuilder;
68 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228.inline.or.keystore.asymmetric.key.grouping.inline.or.keystore.inline.InlineDefinitionBuilder;
69 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev231228.netconf.client.initiate.stack.grouping.transport.tls.tls.TcpClientParametersBuilder;
70 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev231228.netconf.client.listen.stack.grouping.transport.ssh.ssh.SshClientParametersBuilder;
71 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev231228.netconf.client.listen.stack.grouping.transport.ssh.ssh.TcpServerParametersBuilder;
72 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.ClientIdentityBuilder;
73 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev231228.ssh.client.grouping.client.identity.PasswordBuilder;
74 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev231228.SshServerGrouping;
75 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev231228.ssh.server.grouping.ClientAuthentication;
76 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev231228.ssh.server.grouping.ClientAuthenticationBuilder;
77 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev231228.ssh.server.grouping.ServerIdentity;
78 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev231228.ssh.server.grouping.ServerIdentityBuilder;
79 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev231228.ssh.server.grouping.client.authentication.UsersBuilder;
80 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev231228.ssh.server.grouping.client.authentication.users.UserBuilder;
81 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev231228.ssh.server.grouping.server.identity.HostKeyBuilder;
82 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev231228.ssh.server.grouping.server.identity.host.key.host.key.type.PublicKeyBuilder;
83 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev231228.TcpClientGrouping;
84 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev231228.TcpServerGrouping;
85 import org.opendaylight.yangtools.yang.common.Uint16;
87 @ExtendWith(MockitoExtension.class)
88 class NetconfClientFactoryImplTest {
89 private static final String USERNAME = "username";
90 private static final String PASSWORD = "pa$$w0rd";
91 private static final String RSA = "RSA";
92 private static final char[] EMPTY_SECRET = new char[0];
94 private static SSHTransportStackFactory serverTransportFactory;
97 private NetconfClientSessionListener sessionListener;
99 private TransportChannelListener serverTransportListener;
101 private SshServerGrouping sshServerParams;
103 private NetconfClientFactory factory;
104 private TcpServerGrouping tcpServerParams;
105 private TcpClientGrouping tcpClientParams;
109 static void beforeAll() {
110 serverTransportFactory = new SSHTransportStackFactory("server", 0);
114 static void afterAll() {
115 serverTransportFactory.close();
119 void beforeEach() throws Exception {
120 factory = new NetconfClientFactoryImpl();
121 doNothing().when(serverTransportListener).onTransportChannelEstablished(any());
123 // create temp socket to get available port for test
124 final var socket = new ServerSocket(0);
125 final var address = IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress());
126 final var port = new PortNumber(Uint16.valueOf(socket.getLocalPort()));
129 tcpServerParams = new TcpServerParametersBuilder().setLocalAddress(address).setLocalPort(port).build();
131 new TcpClientParametersBuilder().setRemoteAddress(new Host(address)).setRemotePort(port).build();
135 void afterEach() throws Exception {
136 if (factory != null) {
142 void tcpClient() throws Exception {
143 final var server = TCPServer.listen(serverTransportListener,
144 serverTransportFactory.newServerBootstrap(), tcpServerParams).get(1, TimeUnit.SECONDS);
146 final var clientConfig = NetconfClientConfigurationBuilder.create()
147 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TCP)
148 .withTcpParameters(tcpClientParams).withSessionListener(sessionListener).build();
149 assertNotNull(factory.createClient(clientConfig));
150 verify(serverTransportListener, timeout(1000L))
151 .onTransportChannelEstablished(any(TransportChannel.class));
153 server.shutdown().get(1, TimeUnit.SECONDS);
158 void tlsClient() throws Exception {
159 final var keyStore = buildKeystoreWithGeneratedCertificate();
160 final var keyMgr = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
161 keyMgr.init(keyStore, EMPTY_SECRET);
162 final var trustMgr = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
163 trustMgr.init(keyStore);
164 final var serverContext = SslContextBuilder.forServer(keyMgr).trustManager(trustMgr).build();
165 final var clientContext = SslContextBuilder.forClient().keyManager(keyMgr).trustManager(trustMgr).build();
167 final var server = TLSServer.listen(serverTransportListener, serverTransportFactory.newServerBootstrap(),
168 tcpServerParams, channel -> serverContext.newHandler(channel.alloc())).get(1, TimeUnit.SECONDS);
170 final var clientConfig = NetconfClientConfigurationBuilder.create()
171 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TLS)
172 .withTcpParameters(tcpClientParams)
173 .withSslHandlerFactory(channel -> clientContext.newHandler(channel.alloc()))
174 .withSessionListener(sessionListener).build();
175 assertNotNull(factory.createClient(clientConfig));
176 verify(serverTransportListener, timeout(1000L))
177 .onTransportChannelEstablished(any(TransportChannel.class));
179 server.shutdown().get(1, TimeUnit.SECONDS);
183 private static KeyStore buildKeystoreWithGeneratedCertificate() throws Exception {
185 final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
186 keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4), new SecureRandom());
187 final var keyPair = keyPairGenerator.generateKeyPair();
189 final var now = Instant.now();
190 final var contentSigner = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
191 final var x500Name = new X500Name("CN=TestCertificate");
192 final var certificateBuilder = new JcaX509v3CertificateBuilder(x500Name,
193 BigInteger.valueOf(now.toEpochMilli()),
194 Date.from(now), Date.from(now.plus(Duration.ofDays(365))),
196 keyPair.getPublic());
197 final var certificate = new JcaX509CertificateConverter()
198 .setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner));
199 // keystore with certificate and key
200 final var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
201 keyStore.load(null, null);
202 keyStore.setCertificateEntry("cert", certificate);
203 keyStore.setKeyEntry("key", keyPair.getPrivate(), EMPTY_SECRET, new Certificate[]{certificate});
208 void sshClient() throws Exception {
209 doReturn(buildSshServerIdentity()).when(sshServerParams).getServerIdentity();
210 doReturn(buildSshClientAuth()).when(sshServerParams).getClientAuthentication();
211 doReturn(null).when(sshServerParams).getTransportParams();
212 doReturn(null).when(sshServerParams).getKeepalives();
214 final var server = serverTransportFactory.listenServer("netconf", serverTransportListener, tcpServerParams,
215 sshServerParams).get(10, TimeUnit.SECONDS);
218 final var clientConfig = NetconfClientConfigurationBuilder.create()
219 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.SSH)
220 .withTcpParameters(tcpClientParams)
221 .withSshParameters(new SshClientParametersBuilder()
222 .setClientIdentity(new ClientIdentityBuilder().setUsername(USERNAME)
223 .setPassword(new PasswordBuilder().setPasswordType(
224 new CleartextPasswordBuilder().setCleartextPassword(PASSWORD)
225 .build()).build()).build())
227 .withSessionListener(sessionListener)
228 .withConnectionTimeoutMillis(10_000)
230 assertNotNull(factory.createClient(clientConfig));
231 verify(serverTransportListener, timeout(10_000L))
232 .onTransportChannelEstablished(any(TransportChannel.class));
234 server.shutdown().get(1, TimeUnit.SECONDS);
238 private static ServerIdentity buildSshServerIdentity() throws Exception {
239 final var keyPair = KeyPairGenerator.getInstance(RSA).generateKeyPair();
240 final var inlineDef = new InlineDefinitionBuilder()
241 .setPublicKeyFormat(SubjectPublicKeyInfoFormat.VALUE)
242 .setPublicKey(keyPair.getPublic().getEncoded())
243 .setPrivateKeyFormat(RsaPrivateKeyFormat.VALUE)
245 new CleartextPrivateKeyBuilder().setCleartextPrivateKey(
246 keyPair.getPrivate().getEncoded()
249 final var inline = new InlineBuilder().setInlineDefinition(inlineDef).build();
250 final var publicKey = new PublicKeyBuilder().setPublicKey(
251 new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev231228
252 .ssh.server.grouping.server.identity.host.key.host.key.type._public.key
253 .PublicKeyBuilder().setInlineOrKeystore(inline).build()
255 return new ServerIdentityBuilder().setHostKey(
256 List.of(new HostKeyBuilder().setName("test-name").setHostKeyType(publicKey).build())
260 private static ClientAuthentication buildSshClientAuth() {
261 final var user = new UserBuilder().setName(USERNAME).setPassword(new CryptHash("$0$" + PASSWORD)).build();
262 return new ClientAuthenticationBuilder().setUsers(
263 new UsersBuilder().setUser(Map.of(user.key(), user)).build()
268 void sshClientWithConfigurator() throws Exception {
269 final ServerFactoryManagerConfigurator serverConfigurator = factoryManager -> {
270 factoryManager.setUserAuthFactories(List.of(new UserAuthPasswordFactory()));
271 factoryManager.setPasswordAuthenticator(
272 (usr, psw, session) -> USERNAME.equals(usr) && PASSWORD.equals(psw));
273 factoryManager.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
275 final ClientFactoryManagerConfigurator clientConfigurator = factoryManager -> {
276 factoryManager.setPasswordIdentityProvider(PasswordIdentityProvider.wrapPasswords(PASSWORD));
277 factoryManager.setUserAuthFactories(List.of(
278 new org.opendaylight.netconf.shaded.sshd.client.auth.password.UserAuthPasswordFactory()));
281 final var server = serverTransportFactory.listenServer("netconf", serverTransportListener, tcpServerParams,
282 null, serverConfigurator).get(10, TimeUnit.SECONDS);
284 final var clientConfig = NetconfClientConfigurationBuilder.create()
285 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.SSH)
286 .withTcpParameters(tcpClientParams)
287 .withSshParameters(new SshClientParametersBuilder()
288 .setClientIdentity(new ClientIdentityBuilder().setUsername(USERNAME).build()).build())
289 .withSshConfigurator(clientConfigurator)
290 .withSessionListener(sessionListener)
291 .withConnectionTimeoutMillis(10_000)
293 assertNotNull(factory.createClient(clientConfig));
294 verify(serverTransportListener, timeout(10_000L))
295 .onTransportChannelEstablished(any(TransportChannel.class));
297 server.shutdown().get(1, TimeUnit.SECONDS);