Add utility to generate keystore for testing 57/119457/4
authorJim Hahn <jrh3@att.com>
Wed, 17 Mar 2021 22:55:15 +0000 (18:55 -0400)
committerJim Hahn <jrh3@att.com>
Thu, 18 Mar 2021 14:33:31 +0000 (10:33 -0400)
Added a class that will generate a keystore containing a self-signed
certificate.

Issue-ID: POLICY-3147
Change-Id: I25e7307c2e73dbacae24c8fce616bf2ada93df9f
Signed-off-by: Jim Hahn <jrh3@att.com>
utils-test/src/main/java/org/onap/policy/common/utils/security/SelfSignedKeyStore.java [new file with mode: 0644]
utils-test/src/main/resources/keystore_san.txt [new file with mode: 0644]
utils-test/src/test/java/org/onap/policy/common/utils/security/SelfSignedKeyStoreTest.java [new file with mode: 0644]

diff --git a/utils-test/src/main/java/org/onap/policy/common/utils/security/SelfSignedKeyStore.java b/utils-test/src/main/java/org/onap/policy/common/utils/security/SelfSignedKeyStore.java
new file mode 100644 (file)
index 0000000..cc0fed0
--- /dev/null
@@ -0,0 +1,124 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.security;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.ProcessBuilder.Redirect;
+import java.nio.file.Files;
+import java.util.concurrent.TimeUnit;
+import lombok.Getter;
+import org.onap.policy.common.utils.resources.ResourceUtils;
+
+/**
+ * Keystore, containing a self-signed certificate, valid for one day (see the argument to
+ * the "-valid" flag below). For use in junit tests.
+ */
+public class SelfSignedKeyStore {
+    public static final String KEYSTORE_PASSWORD = "Pol1cy_0nap";
+    public static final String PRIVATE_KEY_PASSWORD = KEYSTORE_PASSWORD;
+    public static final String RELATIVE_PATH = "target/test-classes/policy-keystore";
+
+    /**
+     * File containing subject-alternative names (i.e., list of servers that may use this
+     * keystore).
+     */
+    private static final String KEYSTORE_SAN = "keystore_san.txt";
+
+    @Getter
+    private final String keystoreName;
+
+
+    /**
+     * Generates the keystore, if it does not exist or if it's more than a few hours old.
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws InterruptedException if an interrupt occurs
+     */
+    public SelfSignedKeyStore() throws IOException, InterruptedException {
+        this(RELATIVE_PATH);
+    }
+
+    /**
+     * Generates the keystore, if it does not exist or if it's more than a few hours old.
+     *
+     * @param relativePath path to the keystore, relative to the "user.dir" system
+     *        property
+     * @throws IOException if an I/O error occurs
+     * @throws InterruptedException if an interrupt occurs
+     */
+    public SelfSignedKeyStore(String relativePath) throws IOException, InterruptedException {
+        keystoreName = System.getProperty("user.dir") + "/" + relativePath;
+
+        // use existing file if it isn't too old
+        File keystore = new File(keystoreName);
+        if (keystore.exists()) {
+            if (System.currentTimeMillis() < keystore.lastModified()
+                            + TimeUnit.MILLISECONDS.convert(5, TimeUnit.HOURS)) {
+                return;
+            }
+
+            Files.delete(keystore.toPath());
+        }
+
+        /*
+         * Read the list of subject-alternative names, joining the lines with commas, and
+         * dropping the trailing comma.
+         */
+        String sanName = getKeystoreSanName();
+        String subAltNames = ResourceUtils.getResourceAsString(sanName);
+        if (subAltNames == null) {
+            throw new FileNotFoundException(sanName);
+        }
+
+        subAltNames = subAltNames.replace("\r", "").replace("\n", ",");
+        subAltNames = "SAN=" + subAltNames.substring(0, subAltNames.length() - 1);
+
+        // build up the "keytool" command
+
+        // @formatter:off
+        ProcessBuilder builder = new ProcessBuilder("keytool", "-genkeypair",
+                        "-alias", "policy@policy.onap.org",
+                        "-validity", "1",
+                        "-keyalg", "RSA",
+                        "-dname", "C=US, O=ONAP, OU=OSAAF, OU=policy@policy.onap.org:DEV, CN=policy",
+                        "-keystore", keystoreName,
+                        "-keypass", PRIVATE_KEY_PASSWORD,
+                        "-storepass", KEYSTORE_PASSWORD,
+                        "-ext", subAltNames);
+        // @formatter:on
+
+        Process proc = builder.redirectOutput(Redirect.INHERIT).redirectError(Redirect.INHERIT).start();
+        proc.waitFor();
+
+        int exitCode = proc.exitValue();
+        if (exitCode != 0) {
+            throw new IOException("keytool exited with " + exitCode);
+        }
+    }
+
+    // may be overridden by junit tests
+
+    protected String getKeystoreSanName() {
+        return KEYSTORE_SAN;
+    }
+}
diff --git a/utils-test/src/main/resources/keystore_san.txt b/utils-test/src/main/resources/keystore_san.txt
new file mode 100644 (file)
index 0000000..38428ea
--- /dev/null
@@ -0,0 +1,15 @@
+DNS:policy
+DNS:drools
+DNS:drools.onap
+DNS:policy-apex-pdp
+DNS:policy-apex-pdp.onap
+DNS:policy-api
+DNS:policy-api.onap
+DNS:policy-distribution
+DNS:policy-distribution.onap
+DNS:policy-pap
+DNS:policy-pap.onap
+DNS:policy-xacml-pdp
+DNS:policy-xacml-pdp.onap
+DNS:policy.api.simpledemo.onap.org
+DNS:policy-sim
diff --git a/utils-test/src/test/java/org/onap/policy/common/utils/security/SelfSignedKeyStoreTest.java b/utils-test/src/test/java/org/onap/policy/common/utils/security/SelfSignedKeyStoreTest.java
new file mode 100644 (file)
index 0000000..62565bd
--- /dev/null
@@ -0,0 +1,152 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class SelfSignedKeyStoreTest {
+    private static final String USER_DIR_PROP = "user.dir";
+
+    private static String saveUserDir;
+    private static String defaultName;
+    private static File defaultKeystore;
+
+
+    /**
+     * Saves the user.dir property and initializes static fields.
+     */
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        saveUserDir = System.getProperty(USER_DIR_PROP);
+        defaultName = saveUserDir + "/" + SelfSignedKeyStore.RELATIVE_PATH;
+        defaultKeystore = new File(defaultName);
+    }
+
+    @Before
+    public void setUp() {
+        System.setProperty(USER_DIR_PROP, saveUserDir);
+        delete(defaultKeystore);
+    }
+
+    @Test
+    public void testSelfSignedKeyStore() throws Exception {
+        SelfSignedKeyStore ks = new SelfSignedKeyStore();
+
+        assertThat(ks.getKeystoreName()).isEqualTo(defaultName);
+        assertThat(defaultKeystore).exists();
+    }
+
+    @Test
+    public void testSelfSignedKeyStoreString() throws IOException, InterruptedException {
+        String relName = "target/my-keystore";
+        String altName = saveUserDir + "/" + relName;
+        File altFile = new File(altName);
+
+        delete(altFile);
+
+        SelfSignedKeyStore ks = new SelfSignedKeyStore(relName);
+
+        assertThat(ks.getKeystoreName()).isEqualTo(altName);
+        assertThat(altFile).exists();
+    }
+
+    /**
+     * Tests the constructor, when the keystore already exists.
+     */
+    @Test
+    public void testSelfSignedKeyStoreStringExists() throws Exception {
+        new SelfSignedKeyStore();
+        assertThat(defaultKeystore).exists();
+
+        // change the timestamp on the keystore so it's a little old, but not too old
+        defaultKeystore.setLastModified(
+                        System.currentTimeMillis() - TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES));
+        long tcurrent = defaultKeystore.lastModified();
+
+        // this should not recreate the keystore, thus the timestamp should be unchanged
+        new SelfSignedKeyStore();
+        assertThat(defaultKeystore.lastModified()).isEqualTo(tcurrent);
+
+        // change the timestamp on the keystore so it's too old
+        defaultKeystore.setLastModified(
+                        System.currentTimeMillis() - TimeUnit.MILLISECONDS.convert(1000, TimeUnit.HOURS));
+        tcurrent = defaultKeystore.lastModified();
+
+        // this should recreate the keystore
+        new SelfSignedKeyStore();
+        assertThat(defaultKeystore.lastModified()).isGreaterThan(tcurrent);
+    }
+
+    /**
+     * Tests the constructor, when the SAN file is not found.
+     */
+    @Test
+    public void testSelfSignedKeyStoreStringNoSanFile() throws Exception {
+        assertThatThrownBy(() -> new SelfSignedKeyStore() {
+            @Override
+            protected String getKeystoreSanName() {
+                return "unknown/san/file.txt";
+            }
+        }).isInstanceOf(FileNotFoundException.class).hasMessageContaining("file.txt");
+    }
+
+    /**
+     * Tests the constructor, when keytool fails.
+     */
+    @Test
+    public void testSelfSignedKeyStoreStringKeytoolFailure() throws Exception {
+        assertThatThrownBy(() -> new SelfSignedKeyStore("target/unknown/path/to/keystore"))
+                        .isInstanceOf(IOException.class).hasMessageContaining("keytool exited with");
+    }
+
+    @Test
+    public void testGetKeystoreName() throws Exception {
+        String relpath = SelfSignedKeyStore.RELATIVE_PATH;
+
+        // append the first part of the relative path to user.dir
+        System.setProperty(USER_DIR_PROP, saveUserDir + "/" + relpath.substring(0, relpath.indexOf('/')));
+
+        // create a keystore using the remaining components of the relative path
+        SelfSignedKeyStore ks = new SelfSignedKeyStore(relpath.substring(relpath.indexOf('/') + 1));
+
+        assertThat(ks.getKeystoreName()).isEqualTo(defaultName);
+        assertThat(defaultKeystore).exists();
+
+        // try again using the original relative path - should fail, as it's now deeper
+        assertThatThrownBy(() -> new SelfSignedKeyStore(relpath)).isInstanceOf(IOException.class)
+                        .hasMessageContaining("keytool exited with");
+    }
+
+    private static void delete(File file) {
+        if (file.exists()) {
+            assertThat(file.delete()).isTrue();
+        }
+    }
+}