Enforce base64 encoding for netconf-keystore
[netconf.git] / keystore / keystore-legacy / src / main / java / org / opendaylight / netconf / keystore / legacy / impl / DefaultNetconfKeystoreService.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.impl;
9
10 import static java.util.Objects.requireNonNull;
11 import static java.util.Objects.requireNonNullElse;
12
13 import com.google.common.collect.Maps;
14 import java.io.IOException;
15 import java.nio.charset.StandardCharsets;
16 import java.security.GeneralSecurityException;
17 import java.security.KeyPair;
18 import java.security.cert.X509Certificate;
19 import java.util.ArrayList;
20 import java.util.Base64;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.atomic.AtomicReference;
26 import java.util.function.Consumer;
27 import javax.annotation.PreDestroy;
28 import javax.inject.Inject;
29 import javax.inject.Singleton;
30 import org.eclipse.jdt.annotation.NonNull;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
34 import org.opendaylight.mdsal.binding.api.DataBroker;
35 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
36 import org.opendaylight.mdsal.binding.api.RpcProviderService;
37 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
38 import org.opendaylight.mdsal.singleton.api.ClusterSingletonServiceProvider;
39 import org.opendaylight.netconf.keystore.legacy.CertifiedPrivateKey;
40 import org.opendaylight.netconf.keystore.legacy.NetconfKeystore;
41 import org.opendaylight.netconf.keystore.legacy.NetconfKeystoreService;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev231109.Keystore;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev231109._private.keys.PrivateKey;
44 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev231109.keystore.entry.KeyCredential;
45 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev231109.trusted.certificates.TrustedCertificate;
46 import org.opendaylight.yangtools.concepts.AbstractObjectRegistration;
47 import org.opendaylight.yangtools.concepts.Immutable;
48 import org.opendaylight.yangtools.concepts.Mutable;
49 import org.opendaylight.yangtools.concepts.ObjectRegistration;
50 import org.opendaylight.yangtools.concepts.Registration;
51 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
52 import org.osgi.service.component.annotations.Activate;
53 import org.osgi.service.component.annotations.Component;
54 import org.osgi.service.component.annotations.Deactivate;
55 import org.osgi.service.component.annotations.Reference;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59 /**
60  * Abstract substrate for implementing security services based on the contents of {@link Keystore}.
61  */
62 @Singleton
63 @Component(service = NetconfKeystoreService.class)
64 public final class DefaultNetconfKeystoreService implements NetconfKeystoreService, AutoCloseable {
65     @NonNullByDefault
66     private record ConfigState(
67             Map<String, PrivateKey> privateKeys,
68             Map<String, TrustedCertificate> trustedCertificates,
69             Map<String, KeyCredential> credentials) implements Immutable {
70         static final ConfigState EMPTY = new ConfigState(Map.of(), Map.of(), Map.of());
71
72         ConfigState {
73             privateKeys = Map.copyOf(privateKeys);
74             trustedCertificates = Map.copyOf(trustedCertificates);
75             credentials = Map.copyOf(credentials);
76         }
77     }
78
79     @NonNullByDefault
80     record ConfigStateBuilder(
81             HashMap<String, PrivateKey> privateKeys,
82             HashMap<String, TrustedCertificate> trustedCertificates,
83             HashMap<String, KeyCredential> credentials) implements Mutable {
84         ConfigStateBuilder {
85             requireNonNull(privateKeys);
86             requireNonNull(trustedCertificates);
87             requireNonNull(credentials);
88         }
89     }
90
91     private static final Logger LOG = LoggerFactory.getLogger(DefaultNetconfKeystoreService.class);
92
93     private final Set<ObjectRegistration<Consumer<NetconfKeystore>>> consumers = ConcurrentHashMap.newKeySet();
94     private final AtomicReference<NetconfKeystore> keystore = new AtomicReference<>(null);
95     private final AtomicReference<ConfigState> config = new AtomicReference<>(ConfigState.EMPTY);
96     private final SecurityHelper securityHelper = new SecurityHelper();
97     private final AAAEncryptionService encryptionService;
98     private final Registration configListener;
99     private final Registration rpcSingleton;
100
101     @Inject
102     @Activate
103     public DefaultNetconfKeystoreService(@Reference final DataBroker dataBroker,
104             @Reference final RpcProviderService rpcProvider,
105             @Reference final ClusterSingletonServiceProvider cssProvider,
106             @Reference final AAAEncryptionService encryptionService) {
107         this.encryptionService = requireNonNull(encryptionService);
108         configListener = dataBroker.registerTreeChangeListener(
109             DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
110             new ConfigListener(this));
111         rpcSingleton = cssProvider.registerClusterSingletonService(
112             new RpcSingleton(dataBroker, rpcProvider, encryptionService));
113
114         // FIXME: create an operation datastore updater and register it as a consumer
115
116         LOG.info("NETCONF keystore service started");
117     }
118
119     @PreDestroy
120     @Deactivate
121     @Override
122     public void close() {
123         rpcSingleton.close();
124         configListener.close();
125         LOG.info("NETCONF keystore service stopped");
126     }
127
128     @Override
129     public Registration registerKeystoreConsumer(final Consumer<NetconfKeystore> consumer) {
130         final var reg = new AbstractObjectRegistration<>(consumer) {
131             @Override
132             protected void removeRegistration() {
133                 consumers.remove(this);
134             }
135         };
136
137         consumers.add(reg);
138         final var ks = keystore.getAcquire();
139         if (ks != null) {
140             consumer.accept(ks);
141         }
142         return reg;
143     }
144
145     void runUpdate(final Consumer<@NonNull ConfigStateBuilder> task) {
146         final var prevState = config.getAcquire();
147
148         final var builder = new ConfigStateBuilder(new HashMap<>(prevState.privateKeys),
149             new HashMap<>(prevState.trustedCertificates), new HashMap<>(prevState.credentials));
150         task.accept(builder);
151         final var newState = new ConfigState(builder.privateKeys, builder.trustedCertificates, builder.credentials);
152
153         // Careful application -- check if listener is still up and whether the state was not updated.
154         if (configListener == null || config.compareAndExchangeRelease(prevState, newState) != prevState) {
155             return;
156         }
157
158         Throwable failure = null;
159
160         final var keys = Maps.<String, CertifiedPrivateKey>newHashMapWithExpectedSize(newState.privateKeys.size());
161         for (var key : newState.privateKeys.values()) {
162             final var keyName = key.requireName();
163
164             final byte[] keyBytes;
165             try {
166                 keyBytes = base64Decode(new String(key.requireData(), StandardCharsets.UTF_8));
167             } catch (IllegalArgumentException e) {
168                 LOG.debug("Failed to decode private key {}", keyName, e);
169                 failure = updateFailure(failure, e);
170                 continue;
171             }
172
173             final java.security.PrivateKey privateKey;
174             try {
175                 privateKey = securityHelper.generatePrivateKey(keyBytes);
176             } catch (GeneralSecurityException e) {
177                 LOG.debug("Failed to generate key for {}", keyName, e);
178                 failure = updateFailure(failure, e);
179                 continue;
180             }
181
182             final var certChain = key.requireCertificateChain();
183             if (certChain.isEmpty()) {
184                 LOG.debug("Key {} has an empty certificate chain", keyName);
185                 failure = updateFailure(failure,
186                     new IllegalArgumentException("Empty certificate chain for private key " + keyName));
187                 continue;
188             }
189
190             final var certs = new ArrayList<X509Certificate>(certChain.size());
191             for (int i = 0, size = certChain.size(); i < size; i++) {
192                 final byte[] bytes;
193                 try {
194                     bytes = base64Decode(new String(certChain.get(i), StandardCharsets.UTF_8));
195                 } catch (IllegalArgumentException e) {
196                     LOG.debug("Failed to decode certificate chain item {} for private key {}", i, keyName, e);
197                     failure = updateFailure(failure, e);
198                     continue;
199                 }
200
201                 final X509Certificate x509cert;
202                 try {
203                     x509cert = securityHelper.generateCertificate(bytes);
204                 } catch (GeneralSecurityException e) {
205                     LOG.debug("Failed to generate certificate chain item {} for private key {}", i, keyName, e);
206                     failure = updateFailure(failure, e);
207                     continue;
208                 }
209
210                 certs.add(x509cert);
211             }
212
213             keys.put(keyName, new CertifiedPrivateKey(privateKey, certs));
214         }
215
216         final var certs = Maps.<String, X509Certificate>newHashMapWithExpectedSize(newState.trustedCertificates.size());
217         for (var cert : newState.trustedCertificates.values()) {
218             final var certName = cert.requireName();
219
220             final byte[] bytes;
221             try {
222                 bytes = base64Decode(new String(cert.requireCertificate(), StandardCharsets.UTF_8));
223             } catch (IllegalArgumentException e) {
224                 LOG.debug("Failed to decode trusted certificate {}", certName, e);
225                 failure = updateFailure(failure, e);
226                 continue;
227             }
228
229             final X509Certificate x509cert;
230             try {
231                 x509cert = securityHelper.generateCertificate(bytes);
232             } catch (GeneralSecurityException e) {
233                 LOG.debug("Failed to generate certificate for {}", certName, e);
234                 failure = updateFailure(failure, e);
235                 continue;
236             }
237
238             certs.put(certName, x509cert);
239         }
240
241         final var creds = Maps.<String, KeyPair>newHashMapWithExpectedSize(newState.credentials.size());
242         for (var cred : newState.credentials.values()) {
243             final var keyId = cred.requireKeyId();
244             final String passPhrase;
245             try {
246                 passPhrase = decryptString(requireNonNullElse(new String(cred.getPassphrase(), StandardCharsets.UTF_8),
247                     ""));
248             } catch (GeneralSecurityException e) {
249                 LOG.debug("Failed to decrypt pass phrase for {}", keyId, e);
250                 failure = updateFailure(failure, e);
251                 continue;
252             }
253
254             final String privateKey;
255             try {
256                 privateKey = decryptString(new String(cred.getPrivateKey(), StandardCharsets.UTF_8));
257             } catch (GeneralSecurityException e) {
258                 LOG.debug("Failed to decrypt private key for {}", keyId, e);
259                 failure = updateFailure(failure, e);
260                 continue;
261             }
262
263             final KeyPair keyPair;
264             try {
265                 keyPair = securityHelper.decodePrivateKey(privateKey, passPhrase);
266             } catch (IOException e) {
267                 LOG.debug("Failed to decode key pair for {}", keyId, e);
268                 failure = updateFailure(failure, e);
269                 continue;
270             }
271
272             creds.put(keyId, keyPair);
273         }
274
275         if (failure != null) {
276             LOG.warn("New configuration is invalid, not applying it", failure);
277             return;
278         }
279
280         final var newKeystore = new NetconfKeystore(keys, certs, creds);
281         keystore.setRelease(newKeystore);
282         consumers.forEach(consumer -> consumer.getInstance().accept(newKeystore));
283     }
284
285     private static byte[] base64Decode(final String base64) {
286         return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.UTF_8));
287     }
288
289     private String decryptString(final String encrypted) throws GeneralSecurityException {
290         return new String(encryptionService.decrypt(Base64.getDecoder().decode(encrypted)), StandardCharsets.UTF_8);
291     }
292
293     private static @NonNull Throwable updateFailure(final @Nullable Throwable failure, final @NonNull Exception ex) {
294         if (failure != null) {
295             failure.addSuppressed(ex);
296             return failure;
297         }
298         return ex;
299     }
300 }