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
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");
}
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;
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;
- }
- }
}