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