Modify NetconfKeystoreAdapter to expose private keys and certificates 00/68000/10
authorLiGuosheng <li.guosheng6@zte.com.cn>
Wed, 7 Feb 2018 08:24:56 +0000 (16:24 +0800)
committerJakubToth <jakub.toth@pantheon.tech>
Tue, 20 Feb 2018 19:49:55 +0000 (19:49 +0000)
Supply a method of using private keys and trusted certificates to
create JDK KeyStore. Improve code and test cases.

Change-Id: I56dff8cec16eeb320b4765a5d8c5bd3ffe562556
Signed-off-by: Li guosheng <li.guosheng6@zte.com.cn>
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/sal/NetconfKeystoreAdapter.java
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/NetconfKeystoreAdapterTest.java [new file with mode: 0644]

index 8363c54d750cfe009f43699e15a2905ced9ed5ce..6facc7d237001620181ac6cc006748e0b541f84b 100644 (file)
@@ -8,19 +8,37 @@
 
 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;
@@ -33,6 +51,8 @@ public class NetconfKeystoreAdapter implements ClusteredDataTreeChangeListener<K
 
     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;
@@ -46,14 +66,140 @@ public class NetconfKeystoreAdapter implements ClusteredDataTreeChangeListener<K
         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;
         }
     }
 }
diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/NetconfKeystoreAdapterTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/NetconfKeystoreAdapterTest.java
new file mode 100644 (file)
index 0000000..6777c16
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+ * 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"));
+    }
+}