Expose NetconfKeystoreService
[netconf.git] / apps / callhome-provider / src / test / java / org / opendaylight / netconf / callhome / server / tls / 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.callhome.server.tls;
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.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;
39 import java.util.Set;
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;
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 @Nullable String idFor(@NonNull
126             final PublicKey publicKey) {
127                 // identify client 3 only
128                 return clientCert3.keyPair.getPublic().equals(publicKey) ? "client-id" : null;
129             }
130
131             @Override
132             public SslHandler createSslHandler(final Channel channel) {
133                 return serverCtx.newHandler(channel.alloc());
134             }
135         };
136
137         // Netconf layer for clients
138         doReturn(serverSessionListener).when(monitoringService).getSessionListener();
139         doReturn(EMPTY_CAPABILITIES).when(monitoringService).getCapabilities();
140
141         final var timer = new DefaultNetconfTimer();
142
143         final var negotiatorFactory = NetconfServerSessionNegotiatorFactory.builder()
144             .setTimer(timer)
145             .setAggregatedOpService(new AggregatedNetconfOperationServiceFactory())
146             .setIdProvider(new DefaultSessionIdProvider())
147             .setConnectionTimeoutMillis(TIMEOUT)
148             .setMonitoringService(monitoringService)
149             .build();
150         final var netconfTransportListener = new TestNetconfServerInitializer(negotiatorFactory);
151
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();
158
159         // Session context manager
160         final var contextMgr = new CallHomeTlsSessionContextManager(authProvider, statusRecorder) {
161             // inject netconf session listener
162             @Override
163             public CallHomeTlsSessionContext createContext(final String id, final Channel channel) {
164                 return new CallHomeTlsSessionContext(id, channel, clientSessionListener, SettableFuture.create());
165             }
166         };
167
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();
176
177         TLSServer client1 = null;
178         TLSServer client2 = null;
179         TLSServer client3 = null;
180
181         try {
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));
188
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()));
195
196             // client 3 accepted
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");
204
205         } finally {
206             server.close();
207             shutdownClient(client1);
208             shutdownClient(client2);
209             shutdownClient(client3);
210             timer.close();
211         }
212
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");
218     }
219
220     private static void shutdownClient(final @Nullable TLSServer client) throws Exception {
221         if (client != null) {
222             client.shutdown().get(TIMEOUT, TimeUnit.MILLISECONDS);
223         }
224     }
225
226     public static int serverPort() throws Exception {
227         try (var socket = new ServerSocket(0)) {
228             return socket.getLocalPort();
229         }
230     }
231
232     private static CertData generateCertData() throws Exception {
233         // key pair
234         final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
235         keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4), new SecureRandom());
236         final var keyPair = keyPairGenerator.generateKeyPair();
237         // certificate
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))),
244             x500Name,
245             keyPair.getPublic());
246         final var certificate = new JcaX509CertificateConverter()
247             .setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner));
248         return new CertData(keyPair, certificate);
249     }
250
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);
254         return keyManager;
255     }
256
257     private static TrustManagerFactory trustManagerFor(final CertData... certs) throws Exception {
258         final var trustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
259         trustManager.init(keyStoreWithCerts(certs));
260         return trustManager;
261     }
262
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()});
270         }
271         return keyStore;
272     }
273
274     private record CertData(KeyPair keyPair, Certificate certificate) {
275     }
276
277     // Same as org.opendaylight.netconf.server.ServerTransportInitializer but with explicit fireChannelActive()
278     private record TestNetconfServerInitializer(NetconfServerSessionNegotiatorFactory negotiatorFactory)
279         implements TransportChannelListener {
280
281         @Override
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>() {
286                 @Override
287                 protected void initializeSessionNegotiator(final Channel ch,
288                     final Promise<NetconfServerSession> promise) {
289                     ch.pipeline()
290                         .addLast(NETCONF_SESSION_NEGOTIATOR, negotiatorFactory.getSessionNegotiator(ch, promise));
291                 }
292             }.initialize(nettyChannel, nettyChannel.eventLoop().newPromise());
293             // below line is required
294             nettyChannel.pipeline().fireChannelActive();
295         }
296
297         @Override
298         public void onTransportChannelFailed(final Throwable cause) {
299             LOG.error("Call-Home client's transport channel failed", cause);
300         }
301     }
302 }