Flatten callhome-provider
[netconf.git] / apps / callhome-provider / src / test / java / org / opendaylight / netconf / topology / callhome / CallHomeTlsServerTest.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.callhome;
9
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;
17
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;
40 import java.util.Set;
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;
81
82 @ExtendWith(MockitoExtension.class)
83 public class CallHomeTlsServerTest {
84     private static final Logger LOG = LoggerFactory.getLogger("TEST");
85
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();
90
91     @Mock
92     private NetconfMonitoringService monitoringService;
93     @Mock
94     private SessionListener serverSessionListener;
95     @Mock
96     private NetconfClientSessionListener clientSessionListener;
97     @Mock
98     private CallHomeStatusRecorder statusRecorder;
99
100     @Test
101     void integrationTest() throws Exception {
102         // certificates
103         final var serverCert = generateCertData();
104         final var clientCert1 = generateCertData();
105         final var clientCert2 = generateCertData();
106         final var clientCert3 = generateCertData();
107
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();
112
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();
121
122         // Auth provider
123         final var authProvider = new CallHomeTlsAuthProvider() {
124             @Override
125             public String idFor(final PublicKey publicKey) {
126                 // identify client 3 only
127                 return clientCert3.keyPair.getPublic().equals(publicKey) ? "client-id" : null;
128             }
129
130             @Override
131             protected SslContext getSslContext(final SocketAddress remoteAddress) {
132                 return serverCtx;
133             }
134         };
135
136         // Netconf layer for clients
137         doReturn(serverSessionListener).when(monitoringService).getSessionListener();
138         doReturn(EMPTY_CAPABILITIES).when(monitoringService).getCapabilities();
139
140         final var timer = new DefaultNetconfTimer();
141
142         final var negotiatorFactory = NetconfServerSessionNegotiatorFactory.builder()
143             .setTimer(timer)
144             .setAggregatedOpService(new AggregatedNetconfOperationServiceFactory())
145             .setIdProvider(new DefaultSessionIdProvider())
146             .setConnectionTimeoutMillis(TIMEOUT)
147             .setMonitoringService(monitoringService)
148             .build();
149         final var netconfTransportListener = new TestNetconfServerInitializer(negotiatorFactory);
150
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();
157
158         // Session context manager
159         final var contextMgr = new CallHomeTlsSessionContextManager(authProvider, statusRecorder) {
160             // inject netconf session listener
161             @Override
162             public CallHomeTlsSessionContext createContext(final String id, final Channel channel) {
163                 return new CallHomeTlsSessionContext(id, channel, clientSessionListener, SettableFuture.create());
164             }
165         };
166
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();
175
176         TLSServer client1 = null;
177         TLSServer client2 = null;
178         TLSServer client3 = null;
179
180         try {
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));
187
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()));
194
195             // client 3 accepted
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");
203
204         } finally {
205             server.close();
206             shutdownClient(client1);
207             shutdownClient(client2);
208             shutdownClient(client3);
209             timer.close();
210         }
211
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");
217     }
218
219     private static void shutdownClient(final @Nullable TLSServer client) throws Exception {
220         if (client != null) {
221             client.shutdown().get(TIMEOUT, TimeUnit.MILLISECONDS);
222         }
223     }
224
225     public static int serverPort() throws Exception {
226         try (var socket = new ServerSocket(0)) {
227             return socket.getLocalPort();
228         }
229     }
230
231     private static CertData generateCertData() throws Exception {
232         // key pair
233         final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
234         keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4), new SecureRandom());
235         final var keyPair = keyPairGenerator.generateKeyPair();
236         // certificate
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))),
243             x500Name,
244             keyPair.getPublic());
245         final var certificate = new JcaX509CertificateConverter()
246             .setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner));
247         return new CertData(keyPair, certificate);
248     }
249
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);
253         return keyManager;
254     }
255
256     private static TrustManagerFactory trustManagerFor(final CertData... certs) throws Exception {
257         final var trustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
258         trustManager.init(keyStoreWithCerts(certs));
259         return trustManager;
260     }
261
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()});
269         }
270         return keyStore;
271     }
272
273     private record CertData(KeyPair keyPair, Certificate certificate) {
274     }
275
276     // Same as org.opendaylight.netconf.server.ServerTransportInitializer but with explicit fireChannelActive()
277     private record TestNetconfServerInitializer(NetconfServerSessionNegotiatorFactory negotiatorFactory)
278         implements TransportChannelListener {
279
280         @Override
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>() {
285                 @Override
286                 protected void initializeSessionNegotiator(final Channel ch,
287                     final Promise<NetconfServerSession> promise) {
288                     ch.pipeline()
289                         .addLast(NETCONF_SESSION_NEGOTIATOR, negotiatorFactory.getSessionNegotiator(ch, promise));
290                 }
291             }.initialize(nettyChannel, nettyChannel.eventLoop().newPromise());
292             // below line is required
293             nettyChannel.pipeline().fireChannelActive();
294         }
295
296         @Override
297         public void onTransportChannelFailed(final Throwable cause) {
298             LOG.error("Call-Home client's transport channel failed", cause);
299         }
300     }
301 }