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.callhome.server.tls;
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.SslContextBuilder;
22 import io.netty.handler.ssl.SslHandler;
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.security.KeyPair;
29 import java.security.KeyPairGenerator;
30 import java.security.KeyStore;
31 import java.security.PublicKey;
32 import java.security.SecureRandom;
33 import java.security.cert.Certificate;
34 import java.security.spec.RSAKeyGenParameterSpec;
35 import java.time.Duration;
36 import java.time.Instant;
37 import java.util.Date;
38 import java.util.Optional;
40 import java.util.concurrent.TimeUnit;
41 import java.util.concurrent.atomic.AtomicInteger;
42 import javax.net.ssl.KeyManagerFactory;
43 import javax.net.ssl.SSLHandshakeException;
44 import javax.net.ssl.TrustManagerFactory;
45 import org.bouncycastle.asn1.x500.X500Name;
46 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
47 import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
48 import org.bouncycastle.jce.provider.BouncyCastleProvider;
49 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
50 import org.eclipse.jdt.annotation.NonNull;
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.callhome.server.CallHomeStatusRecorder;
57 import org.opendaylight.netconf.client.NetconfClientSession;
58 import org.opendaylight.netconf.client.NetconfClientSessionListener;
59 import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory;
60 import org.opendaylight.netconf.common.impl.DefaultNetconfTimer;
61 import org.opendaylight.netconf.nettyutil.AbstractChannelInitializer;
62 import org.opendaylight.netconf.server.NetconfServerSession;
63 import org.opendaylight.netconf.server.NetconfServerSessionNegotiatorFactory;
64 import org.opendaylight.netconf.server.api.monitoring.NetconfMonitoringService;
65 import org.opendaylight.netconf.server.api.monitoring.SessionListener;
66 import org.opendaylight.netconf.server.impl.DefaultSessionIdProvider;
67 import org.opendaylight.netconf.server.osgi.AggregatedNetconfOperationServiceFactory;
68 import org.opendaylight.netconf.transport.api.TransportChannel;
69 import org.opendaylight.netconf.transport.api.TransportChannelListener;
70 import org.opendaylight.netconf.transport.tcp.BootstrapFactory;
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 @Nullable String idFor(@NonNull
126 final PublicKey publicKey) {
127 // identify client 3 only
128 return clientCert3.keyPair.getPublic().equals(publicKey) ? "client-id" : null;
132 public SslHandler createSslHandler(final Channel channel) {
133 return serverCtx.newHandler(channel.alloc());
137 // Netconf layer for clients
138 doReturn(serverSessionListener).when(monitoringService).getSessionListener();
139 doReturn(EMPTY_CAPABILITIES).when(monitoringService).getCapabilities();
141 final var timer = new DefaultNetconfTimer();
143 final var negotiatorFactory = NetconfServerSessionNegotiatorFactory.builder()
145 .setAggregatedOpService(new AggregatedNetconfOperationServiceFactory())
146 .setIdProvider(new DefaultSessionIdProvider())
147 .setConnectionTimeoutMillis(TIMEOUT)
148 .setMonitoringService(monitoringService)
150 final var netconfTransportListener = new TestNetconfServerInitializer(negotiatorFactory);
152 // tcp layer for clients
153 final var bootstrapFactory = new BootstrapFactory("call-home-test-client", 0);
154 final var serverPort = serverPort();
155 final var tcpConnectParams = new TcpClientParametersBuilder()
156 .setRemoteAddress(new Host(IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress())))
157 .setRemotePort(new PortNumber(Uint16.valueOf(serverPort))).build();
159 // Session context manager
160 final var contextMgr = new CallHomeTlsSessionContextManager(authProvider, statusRecorder) {
161 // inject netconf session listener
163 public CallHomeTlsSessionContext createContext(final String id, final Channel channel) {
164 return new CallHomeTlsSessionContext(id, channel, clientSessionListener, SettableFuture.create());
168 // start Call-Home server
169 final var server = CallHomeTlsServer.builder()
170 .withAuthProvider(authProvider)
171 .withSessionContextManager(contextMgr)
172 .withStatusRecorder(statusRecorder)
173 .withNegotiationFactory(new NetconfClientSessionNegotiatorFactory(timer, Optional.empty(), TIMEOUT,
174 NetconfClientSessionNegotiatorFactory.DEFAULT_CLIENT_CAPABILITIES))
175 .withPort(serverPort).build();
177 TLSServer client1 = null;
178 TLSServer client2 = null;
179 TLSServer client3 = null;
182 // client 1 rejected on handshake, ensure exception
183 client1 = TLSServer.connect(
184 netconfTransportListener, bootstrapFactory.newBootstrap(), tcpConnectParams,
185 channel -> clientCtx1.newHandler(channel.alloc())).get(TIMEOUT, TimeUnit.MILLISECONDS);
186 verify(statusRecorder, timeout(TIMEOUT).times(1))
187 .onTransportChannelFailure(any(SSLHandshakeException.class));
189 // client 2 rejected because it's not identified by public key accepted on handshake stage
190 client2 = TLSServer.connect(
191 netconfTransportListener, bootstrapFactory.newBootstrap(), tcpConnectParams,
192 channel -> clientCtx2.newHandler(channel.alloc())).get(TIMEOUT, TimeUnit.MILLISECONDS);
193 verify(statusRecorder, timeout(TIMEOUT).times(1))
194 .reportUnknown(any(InetSocketAddress.class), eq(clientCert2.keyPair.getPublic()));
197 client3 = TLSServer.connect(
198 netconfTransportListener, bootstrapFactory.newBootstrap(), tcpConnectParams,
199 channel -> clientCtx3.newHandler(channel.alloc())).get(TIMEOUT, TimeUnit.MILLISECONDS);
200 // verify netconf session established
201 verify(clientSessionListener, timeout(TIMEOUT).times(1)).onSessionUp(any(NetconfClientSession.class));
202 verify(serverSessionListener, timeout(TIMEOUT).times(1)).onSessionUp(any(NetconfServerSession.class));
203 verify(statusRecorder, times(1)).reportSuccess("client-id");
207 shutdownClient(client1);
208 shutdownClient(client2);
209 shutdownClient(client3);
213 // validate disconnect reported
214 verify(serverSessionListener, timeout(TIMEOUT).times(1)).onSessionDown(any(NetconfServerSession.class));
215 verify(clientSessionListener, timeout(TIMEOUT).times(1))
216 .onSessionDown(any(NetconfClientSession.class), nullable(Exception.class));
217 verify(statusRecorder, times(1)).reportDisconnected("client-id");
220 private static void shutdownClient(final @Nullable TLSServer client) throws Exception {
221 if (client != null) {
222 client.shutdown().get(TIMEOUT, TimeUnit.MILLISECONDS);
226 public static int serverPort() throws Exception {
227 try (var socket = new ServerSocket(0)) {
228 return socket.getLocalPort();
232 private static CertData generateCertData() throws Exception {
234 final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
235 keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4), new SecureRandom());
236 final var keyPair = keyPairGenerator.generateKeyPair();
238 final var now = Instant.now();
239 final var contentSigner = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
240 final var x500Name = new X500Name("CN=TestCertificate" + COUNTER.incrementAndGet());
241 final var certificateBuilder = new JcaX509v3CertificateBuilder(x500Name,
242 BigInteger.valueOf(now.toEpochMilli()),
243 Date.from(now), Date.from(now.plus(Duration.ofDays(365))),
245 keyPair.getPublic());
246 final var certificate = new JcaX509CertificateConverter()
247 .setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner));
248 return new CertData(keyPair, certificate);
251 private static KeyManagerFactory keyManagerFor(final CertData... certs) throws Exception {
252 final var keyManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
253 keyManager.init(keyStoreWithCerts(certs), EMPTY_SECRET);
257 private static TrustManagerFactory trustManagerFor(final CertData... certs) throws Exception {
258 final var trustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
259 trustManager.init(keyStoreWithCerts(certs));
263 private static KeyStore keyStoreWithCerts(final CertData... certs) throws Exception {
264 final var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
265 keyStore.load(null, null);
266 for (var certData : certs) {
267 keyStore.setCertificateEntry("cert" + COUNTER.incrementAndGet(), certData.certificate());
268 keyStore.setKeyEntry("key" + COUNTER.incrementAndGet(), certData.keyPair().getPrivate(),
269 EMPTY_SECRET, new Certificate[]{certData.certificate()});
274 private record CertData(KeyPair keyPair, Certificate certificate) {
277 // Same as org.opendaylight.netconf.server.ServerTransportInitializer but with explicit fireChannelActive()
278 private record TestNetconfServerInitializer(NetconfServerSessionNegotiatorFactory negotiatorFactory)
279 implements TransportChannelListener {
282 public void onTransportChannelEstablished(final TransportChannel channel) {
283 LOG.debug("Call-Home client's transport channel {} established", channel);
284 final var nettyChannel = channel.channel();
285 new AbstractChannelInitializer<NetconfServerSession>() {
287 protected void initializeSessionNegotiator(final Channel ch,
288 final Promise<NetconfServerSession> promise) {
290 .addLast(NETCONF_SESSION_NEGOTIATOR, negotiatorFactory.getSessionNegotiator(ch, promise));
292 }.initialize(nettyChannel, nettyChannel.eventLoop().newPromise());
293 // below line is required
294 nettyChannel.pipeline().fireChannelActive();
298 public void onTransportChannelFailed(final Throwable cause) {
299 LOG.error("Call-Home client's transport channel failed", cause);