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.callhome;
10 import static org.mockito.ArgumentMatchers.any;
11 import static org.mockito.ArgumentMatchers.eq;
12 import static org.mockito.ArgumentMatchers.nullable;
13 import static org.mockito.Mockito.doReturn;
14 import static org.mockito.Mockito.timeout;
15 import static org.mockito.Mockito.times;
16 import static org.mockito.Mockito.verify;
18 import com.google.common.util.concurrent.SettableFuture;
19 import io.netty.channel.Channel;
20 import io.netty.handler.ssl.ClientAuth;
21 import io.netty.handler.ssl.SslContext;
22 import io.netty.handler.ssl.SslContextBuilder;
23 import io.netty.util.concurrent.Promise;
24 import java.math.BigInteger;
25 import java.net.InetAddress;
26 import java.net.InetSocketAddress;
27 import java.net.ServerSocket;
28 import java.net.SocketAddress;
29 import java.security.KeyPair;
30 import java.security.KeyPairGenerator;
31 import java.security.KeyStore;
32 import java.security.PublicKey;
33 import java.security.SecureRandom;
34 import java.security.cert.Certificate;
35 import java.security.spec.RSAKeyGenParameterSpec;
36 import java.time.Duration;
37 import java.time.Instant;
38 import java.util.Date;
39 import java.util.Optional;
41 import java.util.concurrent.TimeUnit;
42 import java.util.concurrent.atomic.AtomicInteger;
43 import javax.net.ssl.KeyManagerFactory;
44 import javax.net.ssl.SSLHandshakeException;
45 import javax.net.ssl.TrustManagerFactory;
46 import org.bouncycastle.asn1.x500.X500Name;
47 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
48 import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
49 import org.bouncycastle.jce.provider.BouncyCastleProvider;
50 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
51 import org.eclipse.jdt.annotation.Nullable;
52 import org.junit.jupiter.api.Test;
53 import org.junit.jupiter.api.extension.ExtendWith;
54 import org.mockito.Mock;
55 import org.mockito.junit.jupiter.MockitoExtension;
56 import org.opendaylight.netconf.client.NetconfClientSession;
57 import org.opendaylight.netconf.client.NetconfClientSessionListener;
58 import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory;
59 import org.opendaylight.netconf.common.impl.DefaultNetconfTimer;
60 import org.opendaylight.netconf.nettyutil.AbstractChannelInitializer;
61 import org.opendaylight.netconf.server.NetconfServerSession;
62 import org.opendaylight.netconf.server.NetconfServerSessionNegotiatorFactory;
63 import org.opendaylight.netconf.server.api.monitoring.NetconfMonitoringService;
64 import org.opendaylight.netconf.server.api.monitoring.SessionListener;
65 import org.opendaylight.netconf.server.impl.DefaultSessionIdProvider;
66 import org.opendaylight.netconf.server.osgi.AggregatedNetconfOperationServiceFactory;
67 import org.opendaylight.netconf.transport.api.TransportChannel;
68 import org.opendaylight.netconf.transport.api.TransportChannelListener;
69 import org.opendaylight.netconf.transport.tcp.BootstrapFactory;
70 import org.opendaylight.netconf.transport.tls.FixedSslHandlerFactory;
71 import org.opendaylight.netconf.transport.tls.TLSServer;
72 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
73 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
74 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
75 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;
76 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Capabilities;
77 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.CapabilitiesBuilder;
78 import org.opendaylight.yangtools.yang.common.Uint16;
79 import org.slf4j.Logger;
80 import org.slf4j.LoggerFactory;
82 @ExtendWith(MockitoExtension.class)
83 public class CallHomeTlsServerTest {
84 private static final Logger LOG = LoggerFactory.getLogger("TEST");
86 private static final AtomicInteger COUNTER = new AtomicInteger();
87 private static final char[] EMPTY_SECRET = new char[0];
88 private static final long TIMEOUT = 5000L;
89 private static final Capabilities EMPTY_CAPABILITIES = new CapabilitiesBuilder().setCapability(Set.of()).build();
92 private NetconfMonitoringService monitoringService;
94 private SessionListener serverSessionListener;
96 private NetconfClientSessionListener clientSessionListener;
98 private CallHomeStatusRecorder statusRecorder;
101 void integrationTest() throws Exception {
103 final var serverCert = generateCertData();
104 final var clientCert1 = generateCertData();
105 final var clientCert2 = generateCertData();
106 final var clientCert3 = generateCertData();
108 // SSL context for call-home server (acting as client): denies client 1, allows client 2,3
109 final var serverCtx = SslContextBuilder.forClient()
110 .keyManager(keyManagerFor(serverCert))
111 .trustManager(trustManagerFor(clientCert2, clientCert3)).build();
113 // SSL context for call-home clients (acting as servers)
114 final var serverTrustMgr = trustManagerFor(serverCert);
115 final var clientCtx1 = SslContextBuilder.forServer(keyManagerFor(clientCert1))
116 .trustManager(serverTrustMgr).clientAuth(ClientAuth.REQUIRE).build();
117 final var clientCtx2 = SslContextBuilder.forServer(keyManagerFor(clientCert2))
118 .trustManager(serverTrustMgr).clientAuth(ClientAuth.REQUIRE).build();
119 final var clientCtx3 = SslContextBuilder.forServer(keyManagerFor(clientCert3))
120 .trustManager(serverTrustMgr).clientAuth(ClientAuth.REQUIRE).build();
123 final var authProvider = new CallHomeTlsAuthProvider() {
125 public String idFor(final PublicKey publicKey) {
126 // identify client 3 only
127 return clientCert3.keyPair.getPublic().equals(publicKey) ? "client-id" : null;
131 protected SslContext getSslContext(final SocketAddress remoteAddress) {
136 // Netconf layer for clients
137 doReturn(serverSessionListener).when(monitoringService).getSessionListener();
138 doReturn(EMPTY_CAPABILITIES).when(monitoringService).getCapabilities();
140 final var timer = new DefaultNetconfTimer();
142 final var negotiatorFactory = NetconfServerSessionNegotiatorFactory.builder()
144 .setAggregatedOpService(new AggregatedNetconfOperationServiceFactory())
145 .setIdProvider(new DefaultSessionIdProvider())
146 .setConnectionTimeoutMillis(TIMEOUT)
147 .setMonitoringService(monitoringService)
149 final var netconfTransportListener = new TestNetconfServerInitializer(negotiatorFactory);
151 // tcp layer for clients
152 final var bootstrapFactory = new BootstrapFactory("call-home-test-client", 0);
153 final var serverPort = serverPort();
154 final var tcpConnectParams = new TcpClientParametersBuilder()
155 .setRemoteAddress(new Host(IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress())))
156 .setRemotePort(new PortNumber(Uint16.valueOf(serverPort))).build();
158 // Session context manager
159 final var contextMgr = new CallHomeTlsSessionContextManager(authProvider, statusRecorder) {
160 // inject netconf session listener
162 public CallHomeTlsSessionContext createContext(final String id, final Channel channel) {
163 return new CallHomeTlsSessionContext(id, channel, clientSessionListener, SettableFuture.create());
167 // start Call-Home server
168 final var server = CallHomeTlsServer.builder()
169 .withAuthProvider(authProvider)
170 .withSessionContextManager(contextMgr)
171 .withStatusRecorder(statusRecorder)
172 .withNegotiationFactory(new NetconfClientSessionNegotiatorFactory(timer, Optional.empty(), TIMEOUT,
173 NetconfClientSessionNegotiatorFactory.DEFAULT_CLIENT_CAPABILITIES))
174 .withPort(serverPort).build();
176 TLSServer client1 = null;
177 TLSServer client2 = null;
178 TLSServer client3 = null;
181 // client 1 rejected on handshake, ensure exception
182 client1 = TLSServer.connect(
183 netconfTransportListener, bootstrapFactory.newBootstrap(), tcpConnectParams,
184 new FixedSslHandlerFactory(clientCtx1)).get(TIMEOUT, TimeUnit.MILLISECONDS);
185 verify(statusRecorder, timeout(TIMEOUT).times(1))
186 .onTransportChannelFailure(any(SSLHandshakeException.class));
188 // client 2 rejected because it's not identified by public key accepted on handshake stage
189 client2 = TLSServer.connect(
190 netconfTransportListener, bootstrapFactory.newBootstrap(), tcpConnectParams,
191 new FixedSslHandlerFactory(clientCtx2)).get(TIMEOUT, TimeUnit.MILLISECONDS);
192 verify(statusRecorder, timeout(TIMEOUT).times(1))
193 .reportUnknown(any(InetSocketAddress.class), eq(clientCert2.keyPair.getPublic()));
196 client3 = TLSServer.connect(
197 netconfTransportListener, bootstrapFactory.newBootstrap(), tcpConnectParams,
198 new FixedSslHandlerFactory(clientCtx3)).get(TIMEOUT, TimeUnit.MILLISECONDS);
199 // verify netconf session established
200 verify(clientSessionListener, timeout(TIMEOUT).times(1)).onSessionUp(any(NetconfClientSession.class));
201 verify(serverSessionListener, timeout(TIMEOUT).times(1)).onSessionUp(any(NetconfServerSession.class));
202 verify(statusRecorder, times(1)).reportSuccess("client-id");
206 shutdownClient(client1);
207 shutdownClient(client2);
208 shutdownClient(client3);
212 // validate disconnect reported
213 verify(serverSessionListener, timeout(TIMEOUT).times(1)).onSessionDown(any(NetconfServerSession.class));
214 verify(clientSessionListener, timeout(TIMEOUT).times(1))
215 .onSessionDown(any(NetconfClientSession.class), nullable(Exception.class));
216 verify(statusRecorder, times(1)).reportDisconnected("client-id");
219 private static void shutdownClient(final @Nullable TLSServer client) throws Exception {
220 if (client != null) {
221 client.shutdown().get(TIMEOUT, TimeUnit.MILLISECONDS);
225 public static int serverPort() throws Exception {
226 try (var socket = new ServerSocket(0)) {
227 return socket.getLocalPort();
231 private static CertData generateCertData() throws Exception {
233 final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
234 keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4), new SecureRandom());
235 final var keyPair = keyPairGenerator.generateKeyPair();
237 final var now = Instant.now();
238 final var contentSigner = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
239 final var x500Name = new X500Name("CN=TestCertificate" + COUNTER.incrementAndGet());
240 final var certificateBuilder = new JcaX509v3CertificateBuilder(x500Name,
241 BigInteger.valueOf(now.toEpochMilli()),
242 Date.from(now), Date.from(now.plus(Duration.ofDays(365))),
244 keyPair.getPublic());
245 final var certificate = new JcaX509CertificateConverter()
246 .setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner));
247 return new CertData(keyPair, certificate);
250 private static KeyManagerFactory keyManagerFor(final CertData... certs) throws Exception {
251 final var keyManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
252 keyManager.init(keyStoreWithCerts(certs), EMPTY_SECRET);
256 private static TrustManagerFactory trustManagerFor(final CertData... certs) throws Exception {
257 final var trustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
258 trustManager.init(keyStoreWithCerts(certs));
262 private static KeyStore keyStoreWithCerts(final CertData... certs) throws Exception {
263 final var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
264 keyStore.load(null, null);
265 for (var certData : certs) {
266 keyStore.setCertificateEntry("cert" + COUNTER.incrementAndGet(), certData.certificate());
267 keyStore.setKeyEntry("key" + COUNTER.incrementAndGet(), certData.keyPair().getPrivate(),
268 EMPTY_SECRET, new Certificate[]{certData.certificate()});
273 private record CertData(KeyPair keyPair, Certificate certificate) {
276 // Same as org.opendaylight.netconf.server.ServerTransportInitializer but with explicit fireChannelActive()
277 private record TestNetconfServerInitializer(NetconfServerSessionNegotiatorFactory negotiatorFactory)
278 implements TransportChannelListener {
281 public void onTransportChannelEstablished(final TransportChannel channel) {
282 LOG.debug("Call-Home client's transport channel {} established", channel);
283 final var nettyChannel = channel.channel();
284 new AbstractChannelInitializer<NetconfServerSession>() {
286 protected void initializeSessionNegotiator(final Channel ch,
287 final Promise<NetconfServerSession> promise) {
289 .addLast(NETCONF_SESSION_NEGOTIATOR, negotiatorFactory.getSessionNegotiator(ch, promise));
291 }.initialize(nettyChannel, nettyChannel.eventLoop().newPromise());
292 // below line is required
293 nettyChannel.pipeline().fireChannelActive();
297 public void onTransportChannelFailed(final Throwable cause) {
298 LOG.error("Call-Home client's transport channel failed", cause);