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.Collection;
27 import java.util.HashMap;
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.ClusteredDataTreeChangeListener;
35 import org.opendaylight.mdsal.binding.api.DataBroker;
36 import org.opendaylight.mdsal.binding.api.DataObjectModification;
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.rev231121.connection.parameters.protocol.Specification;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev231121.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, ClusteredDataTreeChangeListener<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.registerDataTreeChangeListener(
140 DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
147 public void close() {
152 public SslHandlerFactory getSslHandlerFactory(final Specification specification) {
153 if (specification == null) {
154 return nospecFactory;
156 if (specification instanceof TlsCase tlsSpecification) {
157 final var excludedVersions = tlsSpecification.nonnullTls().getExcludedVersions();
158 return excludedVersions == null || excludedVersions.isEmpty() ? nospecFactory
159 : new SslHandlerFactoryImpl(this, excludedVersions);
161 throw new IllegalArgumentException("Cannot get TLS specification from: " + specification);
165 * Using private keys and trusted certificates to create a new JDK <code>KeyStore</code> which
166 * will be used by TLS clients to create <code>SSLEngine</code>. The private keys are essential
167 * to create JDK <code>KeyStore</code> while the trusted certificates are optional.
169 * @return A JDK KeyStore object
170 * @throws GeneralSecurityException If any security exception occurred
171 * @throws IOException If there is an I/O problem with the keystore data
173 KeyStore getJavaKeyStore() throws GeneralSecurityException, IOException {
174 return getJavaKeyStore(Set.of());
178 * Using private keys and trusted certificates to create a new JDK <code>KeyStore</code> which
179 * will be used by TLS clients to create <code>SSLEngine</code>. The private keys are essential
180 * to create JDK <code>KeyStore</code> while the trusted certificates are optional.
182 * @param allowedKeys Set of keys to include during KeyStore generation, empty set will create
183 * a KeyStore with all possible keys.
184 * @return A JDK KeyStore object
185 * @throws GeneralSecurityException If any security exception occurred
186 * @throws IOException If there is an I/O problem with the keystore data
188 KeyStore getJavaKeyStore(final Set<String> allowedKeys) throws GeneralSecurityException, IOException {
189 requireNonNull(allowedKeys);
190 final var current = state;
191 if (current.privateKeys.isEmpty()) {
192 throw new KeyStoreException("No keystore private key found");
195 final var keyStore = KeyStore.getInstance("JKS");
196 keyStore.load(null, null);
198 final var helper = new SecurityHelper();
200 // Private keys first
201 for (var entry : current.privateKeys.entrySet()) {
202 final var alias = entry.getKey();
203 if (!allowedKeys.isEmpty() && !allowedKeys.contains(alias)) {
207 final var privateKey = entry.getValue();
208 final var key = helper.getJavaPrivateKey(privateKey.getData());
209 // TODO: requireCertificateChain() here and filter in update path
210 final var certChain = privateKey.getCertificateChain();
211 if (certChain == null || certChain.isEmpty()) {
212 throw new CertificateException("No certificate chain associated with private key " + alias + " found");
215 final var chain = new Certificate[certChain.size()];
217 for (var cert : certChain) {
218 chain[idx++] = helper.getCertificate(cert);
220 keyStore.setKeyEntry(alias, key, EMPTY_CHARS, chain);
223 for (var entry : current.trustedCertificates.entrySet()) {
224 keyStore.setCertificateEntry(entry.getKey(), helper.getCertificate(entry.getValue().getCertificate()));
230 private static byte[] base64Decode(final String base64) {
231 return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
235 public void onDataTreeChanged(final Collection<DataTreeModification<Keystore>> changes) {
236 LOG.debug("Starting update with {} changes", changes.size());
237 final var builder = state.newBuilder();
238 onDataTreeChanged(builder, changes);
239 state = builder.build();
240 LOG.debug("Update finished");
243 private static void onDataTreeChanged(final StateBuilder builder,
244 final Collection<DataTreeModification<Keystore>> changes) {
245 for (var change : changes) {
246 LOG.debug("Processing change {}", change);
247 final var rootNode = change.getRootNode();
249 for (var changedChild : rootNode.getModifiedChildren()) {
250 if (changedChild.getDataType().equals(PrivateKey.class)) {
251 onPrivateKeyChanged(builder.privateKeys, (DataObjectModification<PrivateKey>)changedChild);
252 } else if (changedChild.getDataType().equals(TrustedCertificate.class)) {
253 onTrustedCertificateChanged(builder.trustedCertificates,
254 (DataObjectModification<TrustedCertificate>)changedChild);
260 private static void onPrivateKeyChanged(final HashMap<String, PrivateKey> privateKeys,
261 final DataObjectModification<PrivateKey> objectModification) {
262 switch (objectModification.getModificationType()) {
263 case SUBTREE_MODIFIED:
265 final var privateKey = objectModification.getDataAfter();
266 privateKeys.put(privateKey.getName(), privateKey);
269 privateKeys.remove(objectModification.getDataBefore().getName());
276 private static void onTrustedCertificateChanged(final HashMap<String, TrustedCertificate> trustedCertificates,
277 final DataObjectModification<TrustedCertificate> objectModification) {
278 switch (objectModification.getModificationType()) {
279 case SUBTREE_MODIFIED:
281 final var trustedCertificate = objectModification.getDataAfter();
282 trustedCertificates.put(trustedCertificate.getName(), trustedCertificate);
285 trustedCertificates.remove(objectModification.getDataBefore().getName());