d06a62a34e4293a5b72c7c891bb49c80a3713d60
[netconf.git] / keystore / keystore-legacy / src / main / java / org / opendaylight / netconf / keystore / legacy / AbstractNetconfKeystore.java
1 /*
2  * Copyright (c) 2024 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.keystore.legacy;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.collect.Maps;
13 import java.nio.charset.StandardCharsets;
14 import java.security.GeneralSecurityException;
15 import java.security.cert.X509Certificate;
16 import java.util.ArrayList;
17 import java.util.Base64;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.concurrent.atomic.AtomicReference;
22 import java.util.function.Consumer;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
27 import org.opendaylight.mdsal.binding.api.DataBroker;
28 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
29 import org.opendaylight.mdsal.binding.api.RpcProviderService;
30 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
31 import org.opendaylight.mdsal.singleton.api.ClusterSingletonServiceProvider;
32 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
33 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
35 import org.opendaylight.yangtools.concepts.Immutable;
36 import org.opendaylight.yangtools.concepts.Mutable;
37 import org.opendaylight.yangtools.concepts.Registration;
38 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * Abstract substrate for implementing security services based on the contents of {@link Keystore}.
44  */
45 public abstract class AbstractNetconfKeystore {
46     @NonNullByDefault
47     protected record CertifiedPrivateKey(
48             java.security.PrivateKey key,
49             List<X509Certificate> certificateChain) implements Immutable {
50         public CertifiedPrivateKey {
51             requireNonNull(key);
52             certificateChain = List.copyOf(certificateChain);
53             if (certificateChain.isEmpty()) {
54                 throw new IllegalArgumentException("Certificate chain must not be empty");
55             }
56         }
57     }
58
59     @NonNullByDefault
60     protected record State(
61             Map<String, CertifiedPrivateKey> privateKeys,
62             Map<String, X509Certificate> trustedCertificates) implements Immutable {
63         public static final State EMPTY = new State(Map.of(), Map.of());
64
65         public State {
66             privateKeys = Map.copyOf(privateKeys);
67             trustedCertificates = Map.copyOf(trustedCertificates);
68         }
69     }
70
71     @NonNullByDefault
72     private record ConfigState(
73             Map<String, PrivateKey> privateKeys,
74             Map<String, TrustedCertificate> trustedCertificates) implements Immutable {
75         static final ConfigState EMPTY = new ConfigState(Map.of(), Map.of());
76
77         ConfigState {
78             privateKeys = Map.copyOf(privateKeys);
79             trustedCertificates = Map.copyOf(trustedCertificates);
80         }
81     }
82
83     @NonNullByDefault
84     record ConfigStateBuilder(
85             HashMap<String, PrivateKey> privateKeys,
86             HashMap<String, TrustedCertificate> trustedCertificates) implements Mutable {
87         ConfigStateBuilder {
88             requireNonNull(privateKeys);
89             requireNonNull(trustedCertificates);
90         }
91     }
92
93     private static final Logger LOG = LoggerFactory.getLogger(AbstractNetconfKeystore.class);
94
95     private final AtomicReference<@NonNull ConfigState> state = new AtomicReference<>(ConfigState.EMPTY);
96     private final SecurityHelper securityHelper = new SecurityHelper();
97
98     private @Nullable Registration configListener;
99     private @Nullable Registration rpcSingleton;
100
101     protected final void start(final DataBroker dataBroker, final RpcProviderService rpcProvider,
102             final ClusterSingletonServiceProvider cssProvider, final AAAEncryptionService encryptionService) {
103         if (configListener == null) {
104             configListener = dataBroker.registerTreeChangeListener(
105                 DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
106                 new ConfigListener(this));
107             LOG.debug("NETCONF keystore configuration listener started");
108         }
109         if (rpcSingleton == null) {
110             rpcSingleton = cssProvider.registerClusterSingletonService(
111                 new RpcSingleton(dataBroker, rpcProvider, encryptionService));
112             LOG.debug("NETCONF keystore configuration singleton registered");
113         }
114         LOG.info("NETCONF keystore service started");
115     }
116
117     protected final void stop() {
118         final var singleton = rpcSingleton;
119         if (singleton != null) {
120             rpcSingleton = null;
121             singleton.close();
122         }
123         final var listener = configListener;
124         if (listener != null) {
125             configListener = null;
126             listener.close();
127             state.set(ConfigState.EMPTY);
128         }
129     }
130
131     protected abstract void onStateUpdated(@NonNull State newState);
132
133     final void runUpdate(final Consumer<@NonNull ConfigStateBuilder> task) {
134         final var prevState = state.getAcquire();
135
136         final var builder = new ConfigStateBuilder(new HashMap<>(prevState.privateKeys),
137             new HashMap<>(prevState.trustedCertificates));
138         task.accept(builder);
139         final var newState = new ConfigState(builder.privateKeys, builder.trustedCertificates);
140
141         // Careful application -- check if listener is still up and whether the state was not updated.
142         if (configListener == null || state.compareAndExchangeRelease(prevState, newState) != prevState) {
143             return;
144         }
145
146         Throwable failure = null;
147
148         final var keys = Maps.<String, CertifiedPrivateKey>newHashMapWithExpectedSize(newState.privateKeys.size());
149         for (var key : newState.privateKeys.values()) {
150             final var keyName = key.requireName();
151
152             final byte[] keyBytes;
153             try {
154                 keyBytes = base64Decode(key.requireData());
155             } catch (IllegalArgumentException e) {
156                 LOG.debug("Failed to decode private key {}", keyName, e);
157                 failure = updateFailure(failure, e);
158                 continue;
159             }
160
161             final java.security.PrivateKey privateKey;
162             try {
163                 privateKey = securityHelper.generatePrivateKey(keyBytes);
164             } catch (GeneralSecurityException e) {
165                 LOG.debug("Failed to generate key for {}", keyName, e);
166                 failure = updateFailure(failure, e);
167                 continue;
168             }
169
170             final var certChain = key.requireCertificateChain();
171             if (certChain.isEmpty()) {
172                 LOG.debug("Key {} has an empty certificate chain", keyName);
173                 failure = updateFailure(failure,
174                     new IllegalArgumentException("Empty certificate chain for private key " + keyName));
175                 continue;
176             }
177
178             final var certs = new ArrayList<X509Certificate>(certChain.size());
179             for (int i = 0, size = certChain.size(); i < size; i++) {
180                 final byte[] bytes;
181                 try {
182                     bytes = base64Decode(certChain.get(i));
183                 } catch (IllegalArgumentException e) {
184                     LOG.debug("Failed to decode certificate chain item {} for private key {}", i, keyName, e);
185                     failure = updateFailure(failure, e);
186                     continue;
187                 }
188
189                 final X509Certificate x509cert;
190                 try {
191                     x509cert = securityHelper.generateCertificate(bytes);
192                 } catch (GeneralSecurityException e) {
193                     LOG.debug("Failed to generate certificate chain item {} for private key {}", i, keyName, e);
194                     failure = updateFailure(failure, e);
195                     continue;
196                 }
197
198                 certs.add(x509cert);
199             }
200
201             keys.put(keyName, new CertifiedPrivateKey(privateKey, certs));
202         }
203
204         final var certs = Maps.<String, X509Certificate>newHashMapWithExpectedSize(newState.trustedCertificates.size());
205         for (var cert : newState.trustedCertificates.values()) {
206             final var certName = cert.requireName();
207
208             final byte[] bytes;
209             try {
210                 bytes = base64Decode(cert.requireCertificate());
211             } catch (IllegalArgumentException e) {
212                 LOG.debug("Failed to decode trusted certificate {}", certName, e);
213                 failure = updateFailure(failure, e);
214                 continue;
215             }
216
217             final X509Certificate x509cert;
218             try {
219                 x509cert = securityHelper.generateCertificate(bytes);
220             } catch (GeneralSecurityException e) {
221                 LOG.debug("Failed to generate certificate for {}", certName, e);
222                 failure = updateFailure(failure, e);
223                 continue;
224             }
225
226             certs.put(certName, x509cert);
227         }
228
229         if (failure != null) {
230             LOG.warn("New configuration is invalid, not applying it", failure);
231             return;
232         }
233
234         onStateUpdated(new State(keys, certs));
235
236         // FIXME: tickle operational updater (which does not exist yet)
237     }
238
239     private static byte[] base64Decode(final String base64) {
240         return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
241     }
242
243     private static @NonNull Throwable updateFailure(final @Nullable Throwable failure, final @NonNull Exception ex) {
244         if (failure != null) {
245             failure.addSuppressed(ex);
246             return failure;
247         }
248         return ex;
249     }
250 }