package org.opendaylight.netconf.sal.connect.netconf.sal;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+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.ArrayList;
+import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nonnull;
import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener;
import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
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.keystore.entry.KeyCredential;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
+import org.opendaylight.yangtools.yang.binding.DataObject;
import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private final DataBroker dataBroker;
private final Map<String, KeyCredential> pairs = Collections.synchronizedMap(new HashMap<>());
+ private final Map<String, PrivateKey> privateKeys = Collections.synchronizedMap(new HashMap<>());
+ private final Map<String, TrustedCertificate> trustedCertificates = Collections.synchronizedMap(new HashMap<>());
public NetconfKeystoreAdapter(final DataBroker dataBroker) {
this.dataBroker = dataBroker;
return Optional.ofNullable(keypair);
}
+ /**
+ * Using private keys and trusted certificates to create a new JDK <code>KeyStore</code> which
+ * will be used by TLS clients to create <code>SSLEngine</code>. The private keys are essential
+ * to create JDK <code>KeyStore</code> while the trusted certificates are optional.
+ *
+ * @return A JDK KeyStore object
+ * @throws GeneralSecurityException If any security exception occurred
+ * @throws IOException If there is an I/O problem with the keystore data
+ */
+ public java.security.KeyStore getJavaKeyStore() throws GeneralSecurityException, IOException {
+ final java.security.KeyStore keyStore = java.security.KeyStore.getInstance("JKS");
+
+ keyStore.load(null, null);
+
+ synchronized (privateKeys) {
+ if (privateKeys.isEmpty()) {
+ throw new KeyStoreException("No keystore private key found");
+ }
+
+ for (Map.Entry<String, PrivateKey> entry : privateKeys.entrySet()) {
+ final java.security.PrivateKey key = getJavaPrivateKey(entry.getValue().getData());
+
+ final List<X509Certificate> certificateChain =
+ getCertificateChain(entry.getValue().getCertificateChain().toArray(new String[0]));
+ if (certificateChain.isEmpty()) {
+ throw new CertificateException("No certificate chain associated with private key found");
+ }
+
+ keyStore.setKeyEntry(entry.getKey(), key, "".toCharArray(),
+ certificateChain.stream().toArray(Certificate[]::new));
+ }
+ }
+
+ synchronized (trustedCertificates) {
+ for (Map.Entry<String, TrustedCertificate> entry : trustedCertificates.entrySet()) {
+ final List<X509Certificate> x509Certificates =
+ getCertificateChain(new String[] {entry.getValue().getCertificate()});
+
+ keyStore.setCertificateEntry(entry.getKey(), x509Certificates.get(0));
+ }
+ }
+
+ return keyStore;
+ }
+
+ private java.security.PrivateKey getJavaPrivateKey(final String base64PrivateKey)
+ throws GeneralSecurityException {
+ final byte[] encodedKey = base64Decode(base64PrivateKey);
+ final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedKey);
+ java.security.PrivateKey key;
+
+ try {
+ final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ key = keyFactory.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException ignore) {
+ final KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+ key = keyFactory.generatePrivate(keySpec);
+ }
+
+ return key;
+ }
+
+ private List<X509Certificate> getCertificateChain(final String[] base64Certificates)
+ throws GeneralSecurityException {
+ final CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ final List<X509Certificate> certificates = new ArrayList<>();
+
+ for (String cert : base64Certificates) {
+ final byte[] buffer = base64Decode(cert);
+ certificates.add((X509Certificate)factory.generateCertificate(new ByteArrayInputStream(buffer)));
+ }
+
+ return certificates;
+ }
+
+ private byte[] base64Decode(final String base64) {
+ return Base64.getMimeDecoder().decode(base64.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
+ }
+
@Override
public void onDataTreeChanged(@Nonnull final Collection<DataTreeModification<Keystore>> changes) {
LOG.debug("Keystore updated: {}", changes);
- final Keystore dataAfter = changes.iterator().next().getRootNode().getDataAfter();
- pairs.clear();
- if (dataAfter != null) {
- dataAfter.getKeyCredential().forEach(pair -> pairs.put(pair.getKey().getKeyId(), pair));
+ for (final DataTreeModification<Keystore> change : changes) {
+ final DataObjectModification<Keystore> rootNode = change.getRootNode();
+
+ for (final DataObjectModification<? extends DataObject> changedChild : rootNode.getModifiedChildren()) {
+ if (changedChild.getDataType().equals(KeyCredential.class)) {
+ final Keystore dataAfter = rootNode.getDataAfter();
+
+ pairs.clear();
+ if (dataAfter != null) {
+ dataAfter.getKeyCredential().forEach(pair -> pairs.put(pair.getKey().getKeyId(), pair));
+ }
+ break;
+
+ } else if (changedChild.getDataType().equals(PrivateKey.class)) {
+ onPrivateKeyChanged((DataObjectModification<PrivateKey>)changedChild);
+ } else if (changedChild.getDataType().equals(TrustedCertificate.class)) {
+ onTrustedCertificateChanged((DataObjectModification<TrustedCertificate>)changedChild);
+ }
+
+ }
+ }
+ }
+
+ private void onPrivateKeyChanged(final DataObjectModification<PrivateKey> objectModification) {
+
+ switch (objectModification.getModificationType()) {
+ case SUBTREE_MODIFIED:
+ case WRITE:
+ final PrivateKey privateKey = objectModification.getDataAfter();
+ privateKeys.put(privateKey.getName(), privateKey);
+ break;
+ case DELETE:
+ privateKeys.remove(objectModification.getDataBefore().getName());
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void onTrustedCertificateChanged(final DataObjectModification<TrustedCertificate> objectModification) {
+ switch (objectModification.getModificationType()) {
+ case SUBTREE_MODIFIED:
+ case WRITE:
+ final TrustedCertificate trustedCertificate = objectModification.getDataAfter();
+ trustedCertificates.put(trustedCertificate.getName(), trustedCertificate);
+ break;
+ case DELETE:
+ trustedCertificates.remove(objectModification.getDataBefore().getName());
+ break;
+ default:
+ break;
}
}
}
--- /dev/null
+/*
+ * Copyright (c) 2018 ZTE Corporation. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.sal.connect.netconf.sal;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import java.security.KeyStoreException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.controller.config.util.xml.XmlUtil;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
+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._private.keys.PrivateKeyBuilder;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKeyKey;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificateBuilder;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificateKey;
+import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+public class NetconfKeystoreAdapterTest {
+ private static final String XML_ELEMENT_PRIVATE_KEY = "private-key";
+ private static final String XML_ELEMENT_NAME = "name";
+ private static final String XML_ELEMENT_DATA = "data";
+ private static final String XML_ELEMENT_CERT_CHAIN = "certificate-chain";
+ private static final String XML_ELEMENT_TRUSTED_CERT = "trusted-certificate";
+ private static final String XML_ELEMENT_CERT = "certificate";
+
+ @Mock
+ private DataBroker dataBroker;
+ @Mock
+ private ListenerRegistration listenerRegistration;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ doReturn(listenerRegistration).when(dataBroker).registerDataTreeChangeListener(
+ any(DataTreeIdentifier.class), any(NetconfKeystoreAdapter.class));
+ }
+
+ @Test
+ public void testKeystoreAdapterInit() throws Exception {
+ NetconfKeystoreAdapter keystoreAdapter = new NetconfKeystoreAdapter(dataBroker);
+
+ try {
+ keystoreAdapter.getJavaKeyStore();
+ Assert.fail(IllegalStateException.class + "exception expected");
+ } catch (KeyStoreException e) {
+ assertTrue(e.getMessage().startsWith("No keystore private key found"));
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testWritePrivateKey() throws Exception {
+ DataTreeModification<Keystore> dataTreeModification = mock(DataTreeModification.class);
+ DataObjectModification<Keystore> keystoreObjectModification = mock(DataObjectModification.class);
+ doReturn(keystoreObjectModification).when(dataTreeModification).getRootNode();
+
+ DataObjectModification<?> childObjectModification = mock(DataObjectModification.class);
+ doReturn(Collections.singletonList(childObjectModification))
+ .when(keystoreObjectModification).getModifiedChildren();
+ doReturn(PrivateKey.class).when(childObjectModification).getDataType();
+
+ doReturn(DataObjectModification.ModificationType.WRITE)
+ .when(childObjectModification).getModificationType();
+
+ PrivateKey privateKey = getPrivateKey();
+ doReturn(privateKey).when(childObjectModification).getDataAfter();
+
+ NetconfKeystoreAdapter keystoreAdapter = new NetconfKeystoreAdapter(dataBroker);
+ keystoreAdapter.onDataTreeChanged(Collections.singletonList(dataTreeModification));
+
+ java.security.KeyStore keyStore = keystoreAdapter.getJavaKeyStore();
+ assertTrue(keyStore.containsAlias(privateKey.getName()));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testWritePrivateKeyAndTrustedCertificate() throws Exception {
+ // Prepare PrivateKey configuration
+ DataTreeModification<Keystore> dataTreeModification1 = mock(DataTreeModification.class);
+ DataObjectModification<Keystore> keystoreObjectModification1 = mock(DataObjectModification.class);
+ doReturn(keystoreObjectModification1).when(dataTreeModification1).getRootNode();
+
+ DataObjectModification<?> childObjectModification1 = mock(DataObjectModification.class);
+ doReturn(Collections.singletonList(childObjectModification1))
+ .when(keystoreObjectModification1).getModifiedChildren();
+ doReturn(PrivateKey.class).when(childObjectModification1).getDataType();
+
+ doReturn(DataObjectModification.ModificationType.WRITE)
+ .when(childObjectModification1).getModificationType();
+
+ PrivateKey privateKey = getPrivateKey();
+ doReturn(privateKey).when(childObjectModification1).getDataAfter();
+
+ // Prepare TrustedCertificate configuration
+ DataTreeModification<Keystore> dataTreeModification2 = mock(DataTreeModification.class);
+ DataObjectModification<Keystore> keystoreObjectModification2 = mock(DataObjectModification.class);
+ doReturn(keystoreObjectModification2).when(dataTreeModification2).getRootNode();
+
+ DataObjectModification<?> childObjectModification2 = mock(DataObjectModification.class);
+ doReturn(Collections.singletonList(childObjectModification2))
+ .when(keystoreObjectModification2).getModifiedChildren();
+ doReturn(TrustedCertificate.class).when(childObjectModification2).getDataType();
+
+ doReturn(DataObjectModification.ModificationType.WRITE)
+ .when(childObjectModification2).getModificationType();
+
+ TrustedCertificate trustedCertificate = geTrustedCertificate();
+ doReturn(trustedCertificate).when(childObjectModification2).getDataAfter();
+
+ // Apply configurations
+ NetconfKeystoreAdapter keystoreAdapter = new NetconfKeystoreAdapter(dataBroker);
+ keystoreAdapter.onDataTreeChanged(Arrays.asList(dataTreeModification1, dataTreeModification2));
+
+ // Check result
+ java.security.KeyStore keyStore = keystoreAdapter.getJavaKeyStore();
+ assertTrue(keyStore.containsAlias(privateKey.getName()));
+ assertTrue(keyStore.containsAlias(trustedCertificate.getName()));
+ }
+
+ private PrivateKey getPrivateKey() throws Exception {
+ final List<PrivateKey> privateKeys = new ArrayList<>();
+ final Document document = readKeystoreXML();
+ final NodeList nodeList = document.getElementsByTagName(XML_ELEMENT_PRIVATE_KEY);
+ for (int i = 0; i < nodeList.getLength(); i++) {
+ final Node node = nodeList.item(i);
+ if (node.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ final Element element = (Element)node;
+ final String keyName = element.getElementsByTagName(XML_ELEMENT_NAME).item(0).getTextContent();
+ final String keyData = element.getElementsByTagName(XML_ELEMENT_DATA).item(0).getTextContent();
+ final NodeList certNodes = element.getElementsByTagName(XML_ELEMENT_CERT_CHAIN);
+ final List<String> certChain = new ArrayList<>();
+ for (int j = 0; j < certNodes.getLength(); j++) {
+ final Node certNode = certNodes.item(j);
+ if (certNode.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ certChain.add(certNode.getTextContent());
+ }
+
+ final PrivateKey privateKey = new PrivateKeyBuilder()
+ .setKey(new PrivateKeyKey(keyName))
+ .setName(keyName)
+ .setData(keyData)
+ .setCertificateChain(certChain)
+ .build();
+ privateKeys.add(privateKey);
+ }
+
+ return privateKeys.get(0);
+ }
+
+ private TrustedCertificate geTrustedCertificate() throws Exception {
+ final List<TrustedCertificate> trustedCertificates = new ArrayList<>();
+ final Document document = readKeystoreXML();
+ final NodeList nodeList = document.getElementsByTagName(XML_ELEMENT_TRUSTED_CERT);
+ for (int i = 0; i < nodeList.getLength(); i++) {
+ final Node node = nodeList.item(i);
+ if (node.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ final Element element = (Element)node;
+ final String certName = element.getElementsByTagName(XML_ELEMENT_NAME).item(0).getTextContent();
+ final String certData = element.getElementsByTagName(XML_ELEMENT_CERT).item(0).getTextContent();
+
+ final TrustedCertificate certificate = new TrustedCertificateBuilder()
+ .setKey(new TrustedCertificateKey(certName))
+ .setName(certName)
+ .setCertificate(certData)
+ .build();
+ trustedCertificates.add(certificate);
+ }
+
+ return trustedCertificates.get(0);
+ }
+
+ private Document readKeystoreXML() throws Exception {
+ return XmlUtil.readXmlToDocument(getClass().getResourceAsStream("/netconf-keystore.xml"));
+ }
+}