2 * Copyright (c) 2017 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.netconf.client.mdsal.impl;
10 import static java.util.Objects.requireNonNull;
12 import java.io.ByteArrayInputStream;
13 import java.io.IOException;
14 import java.nio.charset.StandardCharsets;
15 import java.security.GeneralSecurityException;
16 import java.security.KeyFactory;
17 import java.security.KeyStore;
18 import java.security.KeyStoreException;
19 import java.security.cert.Certificate;
20 import java.security.cert.CertificateException;
21 import java.security.cert.CertificateFactory;
22 import java.security.cert.X509Certificate;
23 import java.security.spec.InvalidKeySpecException;
24 import java.security.spec.PKCS8EncodedKeySpec;
25 import java.util.Base64;
26 import java.util.HashMap;
27 import java.util.List;
30 import javax.annotation.PreDestroy;
31 import javax.inject.Inject;
32 import javax.inject.Singleton;
33 import org.eclipse.jdt.annotation.NonNull;
34 import org.opendaylight.mdsal.binding.api.DataBroker;
35 import org.opendaylight.mdsal.binding.api.DataObjectModification;
36 import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
37 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
38 import org.opendaylight.mdsal.binding.api.DataTreeModification;
39 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
40 import org.opendaylight.netconf.client.SslHandlerFactory;
41 import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.protocol.Specification;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.protocol.specification.TlsCase;
44 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
45 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
46 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
47 import org.opendaylight.yangtools.concepts.Registration;
48 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
49 import org.osgi.service.component.annotations.Activate;
50 import org.osgi.service.component.annotations.Component;
51 import org.osgi.service.component.annotations.Deactivate;
52 import org.osgi.service.component.annotations.Reference;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 @Component(service = SslHandlerFactoryProvider.class)
58 public final class DefaultSslHandlerFactoryProvider
59 implements SslHandlerFactoryProvider, DataTreeChangeListener<Keystore>, AutoCloseable {
61 * Internal state, updated atomically.
64 @NonNull Map<String, PrivateKey> privateKeys,
65 @NonNull Map<String, TrustedCertificate> trustedCertificates) {
68 requireNonNull(privateKeys);
69 requireNonNull(trustedCertificates);
72 @NonNull StateBuilder newBuilder() {
73 return new StateBuilder(new HashMap<>(privateKeys), new HashMap<>(trustedCertificates));
78 * Intermediate builder for State.
80 private record StateBuilder(
81 @NonNull HashMap<String, PrivateKey> privateKeys,
82 @NonNull HashMap<String, TrustedCertificate> trustedCertificates) {
85 requireNonNull(privateKeys);
86 requireNonNull(trustedCertificates);
89 @NonNull State build() {
90 return new State(Map.copyOf(privateKeys), Map.copyOf(trustedCertificates));
94 private static final class SecurityHelper {
95 private CertificateFactory certFactory;
96 private KeyFactory dsaFactory;
97 private KeyFactory rsaFactory;
99 java.security.PrivateKey getJavaPrivateKey(final String base64PrivateKey) throws GeneralSecurityException {
100 final var keySpec = new PKCS8EncodedKeySpec(base64Decode(base64PrivateKey));
102 if (rsaFactory == null) {
103 rsaFactory = KeyFactory.getInstance("RSA");
106 return rsaFactory.generatePrivate(keySpec);
107 } catch (InvalidKeySpecException ignore) {
111 if (dsaFactory == null) {
112 dsaFactory = KeyFactory.getInstance("DSA");
114 return dsaFactory.generatePrivate(keySpec);
117 private X509Certificate getCertificate(final String base64Certificate) throws GeneralSecurityException {
118 // TODO: https://stackoverflow.com/questions/43809909/is-certificatefactory-getinstancex-509-thread-safe
119 // indicates this is thread-safe in most cases, but can we get a better assurance?
120 if (certFactory == null) {
121 certFactory = CertificateFactory.getInstance("X.509");
123 return (X509Certificate) certFactory.generateCertificate(
124 new ByteArrayInputStream(base64Decode(base64Certificate)));
128 private static final Logger LOG = LoggerFactory.getLogger(DefaultSslHandlerFactoryProvider.class);
129 private static final char[] EMPTY_CHARS = { };
131 private final @NonNull SslHandlerFactory nospecFactory = new SslHandlerFactoryImpl(this, Set.of());
132 private final @NonNull Registration reg;
134 private volatile @NonNull State state = new State(Map.of(), Map.of());
138 public DefaultSslHandlerFactoryProvider(@Reference final DataBroker dataBroker) {
139 reg = dataBroker.registerTreeChangeListener(
140 DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)), this);
146 public void close() {
151 public SslHandlerFactory getSslHandlerFactory(final Specification specification) {
152 if (specification == null) {
153 return nospecFactory;
155 if (specification instanceof TlsCase tlsSpecification) {
156 final var excludedVersions = tlsSpecification.nonnullTls().getExcludedVersions();
157 return excludedVersions == null || excludedVersions.isEmpty() ? nospecFactory
158 : new SslHandlerFactoryImpl(this, excludedVersions);
160 throw new IllegalArgumentException("Cannot get TLS specification from: " + specification);
164 * Using private keys and trusted certificates to create a new JDK <code>KeyStore</code> which
165 * will be used by TLS clients to create <code>SSLEngine</code>. The private keys are essential
166 * to create JDK <code>KeyStore</code> while the trusted certificates are optional.
168 * @return A JDK KeyStore object
169 * @throws GeneralSecurityException If any security exception occurred
170 * @throws IOException If there is an I/O problem with the keystore data
172 KeyStore getJavaKeyStore() throws GeneralSecurityException, IOException {
173 return getJavaKeyStore(Set.of());
177 * Using private keys and trusted certificates to create a new JDK <code>KeyStore</code> which
178 * will be used by TLS clients to create <code>SSLEngine</code>. The private keys are essential
179 * to create JDK <code>KeyStore</code> while the trusted certificates are optional.
181 * @param allowedKeys Set of keys to include during KeyStore generation, empty set will create
182 * a KeyStore with all possible keys.
183 * @return A JDK KeyStore object
184 * @throws GeneralSecurityException If any security exception occurred
185 * @throws IOException If there is an I/O problem with the keystore data
187 KeyStore getJavaKeyStore(final Set<String> allowedKeys) throws GeneralSecurityException, IOException {
188 requireNonNull(allowedKeys);
189 final var current = state;
190 if (current.privateKeys.isEmpty()) {
191 throw new KeyStoreException("No keystore private key found");
194 final var keyStore = KeyStore.getInstance("JKS");
195 keyStore.load(null, null);
197 final var helper = new SecurityHelper();
199 // Private keys first
200 for (var entry : current.privateKeys.entrySet()) {
201 final var alias = entry.getKey();
202 if (!allowedKeys.isEmpty() && !allowedKeys.contains(alias)) {
206 final var privateKey = entry.getValue();
207 final var key = helper.getJavaPrivateKey(privateKey.getData());
208 // TODO: requireCertificateChain() here and filter in update path
209 final var certChain = privateKey.getCertificateChain();
210 if (certChain == null || certChain.isEmpty()) {
211 throw new CertificateException("No certificate chain associated with private key " + alias + " found");
214 final var chain = new Certificate[certChain.size()];
216 for (var cert : certChain) {
217 chain[idx++] = helper.getCertificate(cert);
219 keyStore.setKeyEntry(alias, key, EMPTY_CHARS, chain);
222 for (var entry : current.trustedCertificates.entrySet()) {
223 keyStore.setCertificateEntry(entry.getKey(), helper.getCertificate(entry.getValue().getCertificate()));
229 private static byte[] base64Decode(final String base64) {
230 return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
234 public void onDataTreeChanged(final List<DataTreeModification<Keystore>> changes) {
235 LOG.debug("Starting update with {} changes", changes.size());
236 final var builder = state.newBuilder();
237 onDataTreeChanged(builder, changes);
238 state = builder.build();
239 LOG.debug("Update finished");
242 private static void onDataTreeChanged(final StateBuilder builder,
243 final List<DataTreeModification<Keystore>> changes) {
244 for (var change : changes) {
245 LOG.debug("Processing change {}", change);
246 final var rootNode = change.getRootNode();
248 for (var changedChild : rootNode.modifiedChildren()) {
249 if (changedChild.dataType().equals(PrivateKey.class)) {
250 onPrivateKeyChanged(builder.privateKeys, (DataObjectModification<PrivateKey>)changedChild);
251 } else if (changedChild.dataType().equals(TrustedCertificate.class)) {
252 onTrustedCertificateChanged(builder.trustedCertificates,
253 (DataObjectModification<TrustedCertificate>)changedChild);
259 private static void onPrivateKeyChanged(final HashMap<String, PrivateKey> privateKeys,
260 final DataObjectModification<PrivateKey> objectModification) {
261 switch (objectModification.modificationType()) {
262 case SUBTREE_MODIFIED:
264 final var privateKey = objectModification.dataAfter();
265 privateKeys.put(privateKey.getName(), privateKey);
268 privateKeys.remove(objectModification.dataBefore().getName());
275 private static void onTrustedCertificateChanged(final HashMap<String, TrustedCertificate> trustedCertificates,
276 final DataObjectModification<TrustedCertificate> objectModification) {
277 switch (objectModification.modificationType()) {
278 case SUBTREE_MODIFIED:
280 final var trustedCertificate = objectModification.dataAfter();
281 trustedCertificates.put(trustedCertificate.getName(), trustedCertificate);
284 trustedCertificates.remove(objectModification.dataBefore().getName());