Split out keystore-legacy
[netconf.git] / plugins / netconf-client-mdsal / src / main / java / org / opendaylight / netconf / client / mdsal / impl / DefaultSslHandlerFactoryProvider.java
index 165e5ab710cae74e4c90cfd24f6f584125abde28..64752f664928fc4b44dc5d449ae9bc53427723a0 100644 (file)
@@ -9,142 +9,55 @@ package org.opendaylight.netconf.client.mdsal.impl;
 
 import static java.util.Objects.requireNonNull;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.PKCS8EncodedKeySpec;
-import java.util.Base64;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import javax.annotation.PreDestroy;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.mdsal.binding.api.DataBroker;
-import org.opendaylight.mdsal.binding.api.DataObjectModification;
-import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
-import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
-import org.opendaylight.mdsal.binding.api.DataTreeModification;
-import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.netconf.client.SslHandlerFactory;
 import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider;
+import org.opendaylight.netconf.keystore.legacy.AbstractNetconfKeystore;
+import org.opendaylight.netconf.keystore.legacy.SecurityHelper;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.protocol.Specification;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.protocol.specification.TlsCase;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
-import org.opendaylight.yangtools.concepts.Registration;
-import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Reference;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 @Component(service = SslHandlerFactoryProvider.class)
-public final class DefaultSslHandlerFactoryProvider
-        implements SslHandlerFactoryProvider, DataTreeChangeListener<Keystore>, AutoCloseable {
-    /**
-     * Internal state, updated atomically.
-     */
-    private record State(
-        @NonNull Map<String, PrivateKey> privateKeys,
-        @NonNull Map<String, TrustedCertificate> trustedCertificates) {
-
-        State {
-            requireNonNull(privateKeys);
-            requireNonNull(trustedCertificates);
-        }
-
-        @NonNull StateBuilder newBuilder() {
-            return new StateBuilder(new HashMap<>(privateKeys), new HashMap<>(trustedCertificates));
-        }
-    }
-
-    /**
-     * Intermediate builder for State.
-     */
-    private record StateBuilder(
-        @NonNull HashMap<String, PrivateKey> privateKeys,
-        @NonNull HashMap<String, TrustedCertificate> trustedCertificates) {
-
-        StateBuilder {
-            requireNonNull(privateKeys);
-            requireNonNull(trustedCertificates);
-        }
-
-        @NonNull State build() {
-            return new State(Map.copyOf(privateKeys), Map.copyOf(trustedCertificates));
-        }
-    }
-
-    private static final class SecurityHelper {
-        private CertificateFactory certFactory;
-        private KeyFactory dsaFactory;
-        private KeyFactory rsaFactory;
-
-        java.security.PrivateKey getJavaPrivateKey(final String base64PrivateKey) throws GeneralSecurityException {
-            final var keySpec = new PKCS8EncodedKeySpec(base64Decode(base64PrivateKey));
-
-            if (rsaFactory == null) {
-                rsaFactory = KeyFactory.getInstance("RSA");
-            }
-            try {
-                return rsaFactory.generatePrivate(keySpec);
-            } catch (InvalidKeySpecException ignore) {
-                // Ignored
-            }
-
-            if (dsaFactory == null) {
-                dsaFactory = KeyFactory.getInstance("DSA");
-            }
-            return dsaFactory.generatePrivate(keySpec);
-        }
-
-        private X509Certificate getCertificate(final String base64Certificate) throws GeneralSecurityException {
-            // TODO: https://stackoverflow.com/questions/43809909/is-certificatefactory-getinstancex-509-thread-safe
-            //        indicates this is thread-safe in most cases, but can we get a better assurance?
-            if (certFactory == null) {
-                certFactory = CertificateFactory.getInstance("X.509");
-            }
-            return (X509Certificate) certFactory.generateCertificate(
-                new ByteArrayInputStream(base64Decode(base64Certificate)));
-        }
-    }
-
-    private static final Logger LOG = LoggerFactory.getLogger(DefaultSslHandlerFactoryProvider.class);
+public final class DefaultSslHandlerFactoryProvider extends AbstractNetconfKeystore
+        implements SslHandlerFactoryProvider, AutoCloseable {
     private static final char[] EMPTY_CHARS = { };
 
     private final @NonNull SslHandlerFactory nospecFactory = new SslHandlerFactoryImpl(this, Set.of());
-    private final @NonNull Registration reg;
 
-    private volatile @NonNull State state = new State(Map.of(), Map.of());
+    private volatile @NonNull State state = State.EMPTY;
 
     @Inject
     @Activate
     public DefaultSslHandlerFactoryProvider(@Reference final DataBroker dataBroker) {
-        reg = dataBroker.registerTreeChangeListener(
-            DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)), this);
+        start(dataBroker);
     }
 
     @Deactivate
     @PreDestroy
     @Override
     public void close() {
-        reg.close();
+        stop();
+    }
+
+    @Override
+    protected void onStateUpdated(final State newState) {
+        state = newState;
     }
 
     @Override
@@ -187,7 +100,7 @@ public final class DefaultSslHandlerFactoryProvider
     KeyStore getJavaKeyStore(final Set<String> allowedKeys) throws GeneralSecurityException, IOException {
         requireNonNull(allowedKeys);
         final var current = state;
-        if (current.privateKeys.isEmpty()) {
+        if (current.privateKeys().isEmpty()) {
             throw new KeyStoreException("No keystore private key found");
         }
 
@@ -197,7 +110,7 @@ public final class DefaultSslHandlerFactoryProvider
         final var helper = new SecurityHelper();
 
         // Private keys first
-        for (var entry : current.privateKeys.entrySet()) {
+        for (var entry : current.privateKeys().entrySet()) {
             final var alias = entry.getKey();
             if (!allowedKeys.isEmpty() && !allowedKeys.contains(alias)) {
                 continue;
@@ -219,72 +132,10 @@ public final class DefaultSslHandlerFactoryProvider
             keyStore.setKeyEntry(alias, key, EMPTY_CHARS, chain);
         }
 
-        for (var entry : current.trustedCertificates.entrySet()) {
+        for (var entry : current.trustedCertificates().entrySet()) {
             keyStore.setCertificateEntry(entry.getKey(), helper.getCertificate(entry.getValue().getCertificate()));
         }
 
         return keyStore;
     }
-
-    private static byte[] base64Decode(final String base64) {
-        return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
-    }
-
-    @Override
-    public void onDataTreeChanged(final List<DataTreeModification<Keystore>> changes) {
-        LOG.debug("Starting update with {} changes", changes.size());
-        final var builder = state.newBuilder();
-        onDataTreeChanged(builder, changes);
-        state = builder.build();
-        LOG.debug("Update finished");
-    }
-
-    private static void onDataTreeChanged(final StateBuilder builder,
-            final List<DataTreeModification<Keystore>> changes) {
-        for (var change : changes) {
-            LOG.debug("Processing change {}", change);
-            final var rootNode = change.getRootNode();
-
-            for (var changedChild : rootNode.modifiedChildren()) {
-                if (changedChild.dataType().equals(PrivateKey.class)) {
-                    onPrivateKeyChanged(builder.privateKeys, (DataObjectModification<PrivateKey>)changedChild);
-                } else if (changedChild.dataType().equals(TrustedCertificate.class)) {
-                    onTrustedCertificateChanged(builder.trustedCertificates,
-                        (DataObjectModification<TrustedCertificate>)changedChild);
-                }
-            }
-        }
-    }
-
-    private static void onPrivateKeyChanged(final HashMap<String, PrivateKey> privateKeys,
-            final DataObjectModification<PrivateKey> objectModification) {
-        switch (objectModification.modificationType()) {
-            case SUBTREE_MODIFIED:
-            case WRITE:
-                final var privateKey = objectModification.dataAfter();
-                privateKeys.put(privateKey.getName(), privateKey);
-                break;
-            case DELETE:
-                privateKeys.remove(objectModification.dataBefore().getName());
-                break;
-            default:
-                break;
-        }
-    }
-
-    private static void onTrustedCertificateChanged(final HashMap<String, TrustedCertificate> trustedCertificates,
-            final DataObjectModification<TrustedCertificate> objectModification) {
-        switch (objectModification.modificationType()) {
-            case SUBTREE_MODIFIED:
-            case WRITE:
-                final var trustedCertificate = objectModification.dataAfter();
-                trustedCertificates.put(trustedCertificate.getName(), trustedCertificate);
-                break;
-            case DELETE:
-                trustedCertificates.remove(objectModification.dataBefore().getName());
-                break;
-            default:
-                break;
-        }
-    }
 }