summaryrefslogtreecommitdiffstats
path: root/security-utils
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@oath.com>2018-10-17 16:21:59 +0200
committerBjørn Christian Seime <bjorncs@oath.com>2018-10-19 12:51:59 +0200
commit0bd809390b24df069742b7a3deca824712b4f041 (patch)
tree63796492ac504128a39ff20d5e1be3e33b3b433f /security-utils
parent63c5ed8f8f08c7411214ecbc417451a735aef676 (diff)
Move classes in com.yahoo.security to security-utils
Diffstat (limited to 'security-utils')
-rw-r--r--security-utils/OWNERS1
-rw-r--r--security-utils/pom.xml56
-rw-r--r--security-utils/src/main/java/com/yahoo/security/BasicConstraintsExtension.java14
-rw-r--r--security-utils/src/main/java/com/yahoo/security/BouncyCastleProviderHolder.java14
-rw-r--r--security-utils/src/main/java/com/yahoo/security/Extension.java22
-rw-r--r--security-utils/src/main/java/com/yahoo/security/KeyAlgorithm.java20
-rw-r--r--security-utils/src/main/java/com/yahoo/security/KeyStoreBuilder.java121
-rw-r--r--security-utils/src/main/java/com/yahoo/security/KeyStoreType.java23
-rw-r--r--security-utils/src/main/java/com/yahoo/security/KeyStoreUtils.java34
-rw-r--r--security-utils/src/main/java/com/yahoo/security/KeyUtils.java129
-rw-r--r--security-utils/src/main/java/com/yahoo/security/Pkcs10Csr.java71
-rw-r--r--security-utils/src/main/java/com/yahoo/security/Pkcs10CsrBuilder.java106
-rw-r--r--security-utils/src/main/java/com/yahoo/security/Pkcs10CsrUtils.java38
-rw-r--r--security-utils/src/main/java/com/yahoo/security/SignatureAlgorithm.java22
-rw-r--r--security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java137
-rw-r--r--security-utils/src/main/java/com/yahoo/security/SubjectAlternativeName.java114
-rw-r--r--security-utils/src/main/java/com/yahoo/security/X509CertificateBuilder.java167
-rw-r--r--security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java136
-rw-r--r--security-utils/src/main/java/com/yahoo/security/package-info.java9
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java90
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java66
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/package-info.java8
-rw-r--r--security-utils/src/test/java/com/yahoo/security/KeyStoreBuilderTest.java55
-rw-r--r--security-utils/src/test/java/com/yahoo/security/KeyUtilsTest.java54
-rw-r--r--security-utils/src/test/java/com/yahoo/security/Pkcs10CsrBuilderTest.java27
-rw-r--r--security-utils/src/test/java/com/yahoo/security/Pkcs10CsrTest.java57
-rw-r--r--security-utils/src/test/java/com/yahoo/security/Pkcs10CsrUtilsTest.java30
-rw-r--r--security-utils/src/test/java/com/yahoo/security/SslContextBuilderTest.java77
-rw-r--r--security-utils/src/test/java/com/yahoo/security/TestUtils.java42
-rw-r--r--security-utils/src/test/java/com/yahoo/security/X509CertificateBuilderTest.java83
-rw-r--r--security-utils/src/test/java/com/yahoo/security/X509CertificateUtilsTest.java74
-rw-r--r--security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsTest.java25
-rw-r--r--security-utils/src/test/resources/transport-security-options.json7
33 files changed, 1929 insertions, 0 deletions
diff --git a/security-utils/OWNERS b/security-utils/OWNERS
new file mode 100644
index 00000000000..569bf1cc3a1
--- /dev/null
+++ b/security-utils/OWNERS
@@ -0,0 +1 @@
+bjorncs
diff --git a/security-utils/pom.xml b/security-utils/pom.xml
new file mode 100644
index 00000000000..7006a0f5f86
--- /dev/null
+++ b/security-utils/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0"?>
+<!-- Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <artifactId>security-utils</artifactId>
+ <packaging>container-plugin</packaging>
+ <version>6-SNAPSHOT</version>
+ <dependencies>
+ <!-- provided -->
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>annotations</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- compile scope -->
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>compile</scope>
+ </dependency>
+
+ <!-- test scope -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-library</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/security-utils/src/main/java/com/yahoo/security/BasicConstraintsExtension.java b/security-utils/src/main/java/com/yahoo/security/BasicConstraintsExtension.java
new file mode 100644
index 00000000000..d3c08ba27d0
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/BasicConstraintsExtension.java
@@ -0,0 +1,14 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+/**
+ * @author bjorncs
+ */
+class BasicConstraintsExtension {
+ final boolean isCritical, isCertAuthorityCertificate;
+
+ BasicConstraintsExtension(boolean isCritical, boolean isCertAuthorityCertificate) {
+ this.isCritical = isCritical;
+ this.isCertAuthorityCertificate = isCertAuthorityCertificate;
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/BouncyCastleProviderHolder.java b/security-utils/src/main/java/com/yahoo/security/BouncyCastleProviderHolder.java
new file mode 100644
index 00000000000..48a23a1fe7e
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/BouncyCastleProviderHolder.java
@@ -0,0 +1,14 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+/**
+ * @author bjorncs
+ */
+class BouncyCastleProviderHolder {
+
+ private static final BouncyCastleProvider bcProvider = new BouncyCastleProvider();
+
+ static BouncyCastleProvider getInstance() { return bcProvider; }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/Extension.java b/security-utils/src/main/java/com/yahoo/security/Extension.java
new file mode 100644
index 00000000000..46b781c9c86
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/Extension.java
@@ -0,0 +1,22 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+
+/**
+ * @author bjorncs
+ */
+public enum Extension {
+ BASIC_CONSTRAINTS(org.bouncycastle.asn1.x509.Extension.basicConstraints),
+ SUBJECT_ALTERNATIVE_NAMES(org.bouncycastle.asn1.x509.Extension.subjectAlternativeName);
+
+ final ASN1ObjectIdentifier extensionOId;
+
+ Extension(ASN1ObjectIdentifier extensionOId) {
+ this.extensionOId = extensionOId;
+ }
+
+ public String getOId() {
+ return extensionOId.getId();
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/KeyAlgorithm.java b/security-utils/src/main/java/com/yahoo/security/KeyAlgorithm.java
new file mode 100644
index 00000000000..3218f81f0d6
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/KeyAlgorithm.java
@@ -0,0 +1,20 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+/**
+ * @author bjorncs
+ */
+public enum KeyAlgorithm {
+ RSA("RSA"),
+ EC("EC");
+
+ final String algorithmName;
+
+ KeyAlgorithm(String algorithmName) {
+ this.algorithmName = algorithmName;
+ }
+
+ String getAlgorithmName() {
+ return algorithmName;
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/KeyStoreBuilder.java b/security-utils/src/main/java/com/yahoo/security/KeyStoreBuilder.java
new file mode 100644
index 00000000000..2160fbf6455
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/KeyStoreBuilder.java
@@ -0,0 +1,121 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.util.Collections.singletonList;
+
+/**
+ * @author bjorncs
+ */
+public class KeyStoreBuilder {
+
+ private final List<KeyEntry> keyEntries = new ArrayList<>();
+ private final List<CertificateEntry> certificateEntries = new ArrayList<>();
+
+ private final KeyStoreType keyStoreType;
+ private Path inputFile;
+ private char[] inputFilePassword;
+
+ private KeyStoreBuilder(KeyStoreType keyStoreType) {
+ this.keyStoreType = keyStoreType;
+ }
+
+ public static KeyStoreBuilder withType(KeyStoreType type) {
+ return new KeyStoreBuilder(type);
+ }
+
+ public KeyStoreBuilder fromFile(Path file, char[] password) {
+ this.inputFile = file;
+ this.inputFilePassword = password;
+ return this;
+ }
+
+ public KeyStoreBuilder fromFile(Path file) {
+ return fromFile(file, null);
+ }
+
+ public KeyStoreBuilder withKeyEntry(String alias, PrivateKey privateKey, char[] password, List<X509Certificate> certificateChain) {
+ keyEntries.add(new KeyEntry(alias, privateKey, certificateChain, password));
+ return this;
+ }
+
+ public KeyStoreBuilder withKeyEntry(String alias, PrivateKey privateKey, char[] password, X509Certificate certificate) {
+ return withKeyEntry(alias, privateKey, password, singletonList(certificate));
+ }
+
+ public KeyStoreBuilder withKeyEntry(String alias, PrivateKey privateKey, X509Certificate certificate) {
+ return withKeyEntry(alias, privateKey, null, certificate);
+ }
+
+ public KeyStoreBuilder withKeyEntry(String alias, PrivateKey privateKey, List<X509Certificate> certificateChain) {
+ return withKeyEntry(alias, privateKey, null, certificateChain);
+ }
+
+ public KeyStoreBuilder withCertificateEntry(String alias, X509Certificate certificate) {
+ certificateEntries.add(new CertificateEntry(alias, certificate));
+ return this;
+ }
+
+ public KeyStore build() {
+ try {
+ KeyStore keystore = this.keyStoreType.createKeystore();
+ if (this.inputFile != null) {
+ try (InputStream in = new BufferedInputStream(Files.newInputStream(this.inputFile))) {
+ keystore.load(in, this.inputFilePassword);
+ }
+ } else {
+ keystore.load(null);
+ }
+ for (KeyEntry entry : keyEntries) {
+ char[] password = entry.password != null ? entry.password : new char[0];
+ Certificate[] certificateChain = entry.certificateChain.toArray(new Certificate[entry.certificateChain.size()]);
+ keystore.setKeyEntry(entry.alias, entry.privateKey, password, certificateChain);
+ }
+ for (CertificateEntry entry : certificateEntries) {
+ keystore.setCertificateEntry(entry.alias, entry.certificate);
+ }
+ return keystore;
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static class KeyEntry {
+ final String alias;
+ final PrivateKey privateKey;
+ final List<X509Certificate> certificateChain;
+ final char[] password;
+
+ KeyEntry(String alias, PrivateKey privateKey, List<X509Certificate> certificateChain, char[] password) {
+ this.alias = alias;
+ this.privateKey = privateKey;
+ this.certificateChain = certificateChain;
+ this.password = password;
+ }
+ }
+
+ private static class CertificateEntry {
+ final String alias;
+ final X509Certificate certificate;
+
+ CertificateEntry(String alias, X509Certificate certificate) {
+ this.alias = alias;
+ this.certificate = certificate;
+ }
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/KeyStoreType.java b/security-utils/src/main/java/com/yahoo/security/KeyStoreType.java
new file mode 100644
index 00000000000..7fb8df35286
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/KeyStoreType.java
@@ -0,0 +1,23 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+
+/**
+ * @author bjorncs
+ */
+public enum KeyStoreType {
+ JKS {
+ KeyStore createKeystore() throws KeyStoreException {
+ return KeyStore.getInstance("JKS");
+ }
+ },
+ PKCS12 {
+ KeyStore createKeystore() throws KeyStoreException {
+ return KeyStore.getInstance("PKCS12", BouncyCastleProviderHolder.getInstance());
+ }
+ };
+ abstract KeyStore createKeystore() throws GeneralSecurityException;
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/KeyStoreUtils.java b/security-utils/src/main/java/com/yahoo/security/KeyStoreUtils.java
new file mode 100644
index 00000000000..f0c4d99bf69
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/KeyStoreUtils.java
@@ -0,0 +1,34 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+
+/**
+ * @author bjorncs
+ */
+public class KeyStoreUtils {
+ private KeyStoreUtils() {}
+
+ public static void writeKeyStoreToFile(KeyStore keyStore, Path file, char[] password) {
+ try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(file))) {
+ keyStore.store(out, password);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ public static void writeKeyStoreToFile(KeyStore keyStore, Path file) {
+ writeKeyStoreToFile(keyStore, file, new char[0]);
+ }
+
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/KeyUtils.java b/security-utils/src/main/java/com/yahoo/security/KeyUtils.java
new file mode 100644
index 00000000000..11fb0f432e4
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/KeyUtils.java
@@ -0,0 +1,129 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1Primitive;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
+import org.bouncycastle.jce.spec.ECParameterSpec;
+import org.bouncycastle.jce.spec.ECPublicKeySpec;
+import org.bouncycastle.math.ec.ECPoint;
+import org.bouncycastle.math.ec.FixedPointCombMultiplier;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.util.io.pem.PemObject;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.UncheckedIOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.RSAPrivateCrtKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+
+import static com.yahoo.security.KeyAlgorithm.EC;
+import static com.yahoo.security.KeyAlgorithm.RSA;
+
+/**
+ * @author bjorncs
+ */
+public class KeyUtils {
+ private KeyUtils() {}
+
+ public static KeyPair generateKeypair(KeyAlgorithm algorithm, int keySize) {
+ try {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm.getAlgorithmName(), BouncyCastleProviderHolder.getInstance());
+ if (keySize != -1) {
+ keyGen.initialize(keySize);
+ }
+ return keyGen.genKeyPair();
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static KeyPair generateKeypair(KeyAlgorithm algorithm) {
+ return generateKeypair(algorithm, -1);
+ }
+
+ public static PublicKey extractPublicKey(PrivateKey privateKey) {
+ String algorithm = privateKey.getAlgorithm();
+ try {
+ if (algorithm.equals(RSA.getAlgorithmName())) {
+ KeyFactory keyFactory = KeyFactory.getInstance(RSA.getAlgorithmName(), BouncyCastleProviderHolder.getInstance());
+ RSAPrivateCrtKey rsaPrivateCrtKey = (RSAPrivateCrtKey) privateKey;
+ RSAPublicKeySpec keySpec = new RSAPublicKeySpec(rsaPrivateCrtKey.getModulus(), rsaPrivateCrtKey.getPublicExponent());
+ return keyFactory.generatePublic(keySpec);
+ } else if (algorithm.equals(EC.getAlgorithmName())) {
+ KeyFactory keyFactory = KeyFactory.getInstance(EC.getAlgorithmName(), BouncyCastleProviderHolder.getInstance());
+ BCECPrivateKey ecPrivateKey = (BCECPrivateKey) privateKey;
+ ECParameterSpec ecParameterSpec = ecPrivateKey.getParameters();
+ ECPoint ecPoint = new FixedPointCombMultiplier().multiply(ecParameterSpec.getG(), ecPrivateKey.getD());
+ ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameterSpec);
+ return keyFactory.generatePublic(keySpec);
+ } else {
+ throw new IllegalArgumentException("Unexpected key algorithm: " + algorithm);
+ }
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static PrivateKey fromPemEncodedPrivateKey(String pem) {
+ try (PEMParser parser = new PEMParser(new StringReader(pem))) {
+ Object pemObject = parser.readObject();
+ if (pemObject instanceof PrivateKeyInfo) {
+ PrivateKeyInfo keyInfo = (PrivateKeyInfo) pemObject;
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyInfo.getEncoded());
+ return KeyFactory.getInstance(RSA.getAlgorithmName()).generatePrivate(keySpec);
+ } else if (pemObject instanceof PEMKeyPair) {
+ PEMKeyPair pemKeypair = (PEMKeyPair) pemObject;
+ PrivateKeyInfo keyInfo = pemKeypair.getPrivateKeyInfo();
+ JcaPEMKeyConverter pemConverter = new JcaPEMKeyConverter().setProvider(BouncyCastleProviderHolder.getInstance());
+ return pemConverter.getPrivateKey(keyInfo);
+ }
+ throw new IllegalArgumentException("Unexpected type of PEM type: " + pemObject);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String toPem(PrivateKey privateKey) {
+ try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
+ String algorithm = privateKey.getAlgorithm();
+ // Note: Encoding using PKCS#1 as this is to be read by tools only supporting PKCS#1
+ String type;
+ if (algorithm.equals(RSA.getAlgorithmName())) {
+ type = "RSA PRIVATE KEY";
+ } else if (algorithm.equals(EC.getAlgorithmName())) {
+ type = "EC PRIVATE KEY";
+ } else {
+ throw new IllegalArgumentException("Unexpected key algorithm: " + algorithm);
+ }
+ pemWriter.writeObject(new PemObject(type, getPkcs1Bytes(privateKey)));
+ pemWriter.flush();
+ return stringWriter.toString();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static byte[] getPkcs1Bytes(PrivateKey privateKey) throws IOException{
+
+ byte[] privBytes = privateKey.getEncoded();
+ PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(privBytes);
+ ASN1Encodable encodable = pkInfo.parsePrivateKey();
+ ASN1Primitive primitive = encodable.toASN1Primitive();
+ return primitive.getEncoded();
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/Pkcs10Csr.java b/security-utils/src/main/java/com/yahoo/security/Pkcs10Csr.java
new file mode 100644
index 00000000000..e08ee117fcd
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/Pkcs10Csr.java
@@ -0,0 +1,71 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.Extensions;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.pkcs.PKCS10CertificationRequest;
+
+import javax.security.auth.x500.X500Principal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static java.util.Collections.emptyList;
+import static java.util.stream.Collectors.toList;
+
+/**
+ * @author bjorncs
+ */
+public class Pkcs10Csr {
+
+ private final PKCS10CertificationRequest csr;
+
+ Pkcs10Csr(PKCS10CertificationRequest csr) {
+ this.csr = csr;
+ }
+
+ PKCS10CertificationRequest getBcCsr() {
+ return csr;
+ }
+
+ public X500Principal getSubject() {
+ return new X500Principal(csr.getSubject().toString());
+ }
+
+ public List<SubjectAlternativeName> getSubjectAlternativeNames() {
+ return getExtensions()
+ .map(extensions -> GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName))
+ .map(SubjectAlternativeName::fromGeneralNames)
+ .orElse(emptyList());
+ }
+
+ /**
+ * @return If basic constraints extension is present: returns true if CA cert, false otherwise. Returns empty if the extension is not present.
+ */
+ public Optional<Boolean> getBasicConstraints() {
+ return getExtensions()
+ .map(BasicConstraints::fromExtensions)
+ .map(BasicConstraints::isCA);
+ }
+
+ public List<String> getExtensionOIds() {
+ return getExtensions()
+ .map(extensions -> Arrays.stream(extensions.getExtensionOIDs())
+ .map(ASN1ObjectIdentifier::getId)
+ .collect(toList()))
+ .orElse(emptyList());
+
+ }
+
+ private Optional<Extensions> getExtensions() {
+ return Optional.of(csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest))
+ .filter(attributes -> attributes.length > 0)
+ .map(attributes -> attributes[0])
+ .map(attribute -> Extensions.getInstance(attribute.getAttrValues().getObjectAt(0)));
+ }
+
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/Pkcs10CsrBuilder.java b/security-utils/src/main/java/com/yahoo/security/Pkcs10CsrBuilder.java
new file mode 100644
index 00000000000..0fe6117ef2f
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/Pkcs10CsrBuilder.java
@@ -0,0 +1,106 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.ExtensionsGenerator;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.security.KeyPair;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME;
+
+/**
+ * @author bjorncs
+ */
+public class Pkcs10CsrBuilder {
+
+ private final X500Principal subject;
+ private final KeyPair keyPair;
+ private final List<SubjectAlternativeName> subjectAlternativeNames = new ArrayList<>();
+ private final SignatureAlgorithm signatureAlgorithm;
+ private BasicConstraintsExtension basicConstraintsExtension;
+
+ private Pkcs10CsrBuilder(X500Principal subject,
+ KeyPair keyPair,
+ SignatureAlgorithm signatureAlgorithm) {
+ this.subject = subject;
+ this.keyPair = keyPair;
+ this.signatureAlgorithm = signatureAlgorithm;
+ }
+
+ public static Pkcs10CsrBuilder fromKeypair(X500Principal subject,
+ KeyPair keyPair,
+ SignatureAlgorithm signatureAlgorithm) {
+ return new Pkcs10CsrBuilder(subject, keyPair, signatureAlgorithm);
+ }
+
+ public Pkcs10CsrBuilder addSubjectAlternativeName(String dns) {
+ this.subjectAlternativeNames.add(new SubjectAlternativeName(DNS_NAME, dns));
+ return this;
+ }
+
+ public Pkcs10CsrBuilder addSubjectAlternativeName(SubjectAlternativeName san) {
+ this.subjectAlternativeNames.add(san);
+ return this;
+ }
+
+ public Pkcs10CsrBuilder addSubjectAlternativeName(SubjectAlternativeName.Type type, String value) {
+ this.subjectAlternativeNames.add(new SubjectAlternativeName(type, value));
+ return this;
+ }
+
+ public Pkcs10CsrBuilder setBasicConstraints(boolean isCritical, boolean isCertAuthorityCertificate) {
+ this.basicConstraintsExtension = new BasicConstraintsExtension(isCritical, isCertAuthorityCertificate);
+ return this;
+ }
+
+ public Pkcs10CsrBuilder setIsCertAuthority(boolean isCertAuthority) {
+ return setBasicConstraints(true, isCertAuthority);
+ }
+
+ public Pkcs10Csr build() {
+ try {
+ PKCS10CertificationRequestBuilder requestBuilder =
+ new JcaPKCS10CertificationRequestBuilder(new X500Name(subject.getName()), keyPair.getPublic());
+ ExtensionsGenerator extGen = new ExtensionsGenerator();
+ if (basicConstraintsExtension != null) {
+ extGen.addExtension(
+ Extension.basicConstraints,
+ basicConstraintsExtension.isCritical,
+ new BasicConstraints(basicConstraintsExtension.isCertAuthorityCertificate));
+ }
+ if (!subjectAlternativeNames.isEmpty()) {
+ GeneralNames generalNames = new GeneralNames(
+ subjectAlternativeNames.stream()
+ .map(SubjectAlternativeName::toGeneralName)
+ .toArray(GeneralName[]::new));
+ extGen.addExtension(Extension.subjectAlternativeName, false, generalNames);
+ }
+ requestBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extGen.generate());
+ ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm.getAlgorithmName())
+ .setProvider(BouncyCastleProviderHolder.getInstance())
+ .build(keyPair.getPrivate());
+ return new Pkcs10Csr(requestBuilder.build(contentSigner));
+ } catch (OperatorCreationException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+
+ }
+
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/Pkcs10CsrUtils.java b/security-utils/src/main/java/com/yahoo/security/Pkcs10CsrUtils.java
new file mode 100644
index 00000000000..6f12450528d
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/Pkcs10CsrUtils.java
@@ -0,0 +1,38 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.pkcs.PKCS10CertificationRequest;
+import org.bouncycastle.util.io.pem.PemObject;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.UncheckedIOException;
+
+/**
+ * @author bjorncs
+ */
+public class Pkcs10CsrUtils {
+
+ private Pkcs10CsrUtils() {}
+
+ public static Pkcs10Csr fromPem(String pem) {
+ try (PEMParser pemParser = new PEMParser(new StringReader(pem))) {
+ return new Pkcs10Csr((PKCS10CertificationRequest) pemParser.readObject());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public static String toPem(Pkcs10Csr csr) {
+ try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
+ pemWriter.writeObject(new PemObject("CERTIFICATE REQUEST", csr.getBcCsr().getEncoded()));
+ pemWriter.flush();
+ return stringWriter.toString();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/SignatureAlgorithm.java b/security-utils/src/main/java/com/yahoo/security/SignatureAlgorithm.java
new file mode 100644
index 00000000000..fbff18f5c12
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/SignatureAlgorithm.java
@@ -0,0 +1,22 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+/**
+ * @author bjorncs
+ */
+public enum SignatureAlgorithm {
+ SHA256_WITH_RSA("SHA256withRSA"),
+ SHA512_WITH_RSA("SHA512withRSA"),
+ SHA256_WITH_ECDSA("SHA256withECDSA"),
+ SHA512_WITH_ECDSA("SHA512withECDSA");
+
+ private final String algorithmName;
+
+ SignatureAlgorithm(String algorithmName) {
+ this.algorithmName = algorithmName;
+ }
+
+ public String getAlgorithmName() {
+ return algorithmName;
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java b/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java
new file mode 100644
index 00000000000..75ab2417edf
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java
@@ -0,0 +1,137 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.List;
+
+import static java.util.Collections.singletonList;
+
+/**
+ * @author bjorncs
+ */
+public class SslContextBuilder {
+
+ private KeyStoreSupplier trustStoreSupplier;
+ private KeyStoreSupplier keyStoreSupplier;
+ private char[] keyStorePassword;
+
+ public SslContextBuilder() {}
+
+ public SslContextBuilder withTrustStore(Path file, KeyStoreType trustStoreType) {
+ this.trustStoreSupplier = () -> KeyStoreBuilder.withType(trustStoreType).fromFile(file).build();
+ return this;
+ }
+
+ public SslContextBuilder withTrustStore(KeyStore trustStore) {
+ this.trustStoreSupplier = () -> trustStore;
+ return this;
+ }
+
+ public SslContextBuilder withTrustStore(X509Certificate caCertificate) {
+ return withTrustStore(singletonList(caCertificate));
+ }
+
+ public SslContextBuilder withTrustStore(List<X509Certificate> caCertificates) {
+ this.trustStoreSupplier = () -> createTrustStore(caCertificates);
+ return this;
+ }
+
+ public SslContextBuilder withTrustStore(Path pemEncodedCaCertificates) {
+ this.trustStoreSupplier = () -> {
+ List<X509Certificate> caCertificates =
+ X509CertificateUtils.certificateListFromPem(new String(Files.readAllBytes(pemEncodedCaCertificates)));
+ return createTrustStore(caCertificates);
+ };
+ return this;
+ }
+
+ public SslContextBuilder withKeyStore(PrivateKey privateKey, X509Certificate certificate) {
+ char[] pwd = new char[0];
+ this.keyStoreSupplier = () -> KeyStoreBuilder.withType(KeyStoreType.JKS).withKeyEntry("default", privateKey, certificate).build();
+ this.keyStorePassword = pwd;
+ return this;
+ }
+
+ public SslContextBuilder withKeyStore(KeyStore keyStore, char[] password) {
+ this.keyStoreSupplier = () -> keyStore;
+ this.keyStorePassword = password;
+ return this;
+ }
+
+ public SslContextBuilder withKeyStore(Path file, char[] password, KeyStoreType keyStoreType) {
+ this.keyStoreSupplier = () -> KeyStoreBuilder.withType(keyStoreType).fromFile(file, password).build();
+ this.keyStorePassword = password;
+ return this;
+ }
+
+ public SslContextBuilder withKeyStore(Path privateKeyPemFile, Path certificatesPemFile) {
+ this.keyStoreSupplier =
+ () -> {
+ PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(new String(Files.readAllBytes(privateKeyPemFile)));
+ List<X509Certificate> certificates = X509CertificateUtils.certificateListFromPem(new String(Files.readAllBytes(certificatesPemFile)));
+ return KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .withKeyEntry("default", privateKey, certificates)
+ .build();
+ };
+ this.keyStorePassword = new char[0];
+ return this;
+ }
+
+ public SSLContext build() {
+ try {
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+ TrustManager[] trustManagers =
+ trustStoreSupplier != null ? createTrustManagers(trustStoreSupplier) : null;
+ KeyManager[] keyManagers =
+ keyStoreSupplier != null ? createKeyManagers(keyStoreSupplier, keyStorePassword) : null;
+ sslContext.init(keyManagers, trustManagers, null);
+ return sslContext;
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static TrustManager[] createTrustManagers(KeyStoreSupplier trustStoreSupplier)
+ throws GeneralSecurityException, IOException {
+ TrustManagerFactory trustManagerFactory =
+ TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(trustStoreSupplier.get());
+ return trustManagerFactory.getTrustManagers();
+ }
+
+ private static KeyManager[] createKeyManagers(KeyStoreSupplier keyStoreSupplier, char[] password)
+ throws GeneralSecurityException, IOException {
+ KeyManagerFactory keyManagerFactory =
+ KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keyStoreSupplier.get(), password);
+ return keyManagerFactory.getKeyManagers();
+ }
+
+ private static KeyStore createTrustStore(List<X509Certificate> caCertificates) {
+ KeyStoreBuilder trustStoreBuilder = KeyStoreBuilder.withType(KeyStoreType.JKS);
+ for (int i = 0; i < caCertificates.size(); i++) {
+ trustStoreBuilder.withCertificateEntry("cert-" + i, caCertificates.get(i));
+ }
+ return trustStoreBuilder.build();
+ }
+
+ private interface KeyStoreSupplier {
+ KeyStore get() throws IOException, GeneralSecurityException;
+ }
+
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/SubjectAlternativeName.java b/security-utils/src/main/java/com/yahoo/security/SubjectAlternativeName.java
new file mode 100644
index 00000000000..29395c75e70
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/SubjectAlternativeName.java
@@ -0,0 +1,114 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.DERIA5String;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * @author bjorncs
+ */
+public class SubjectAlternativeName {
+
+ private final Type type;
+ private final String value;
+
+ public SubjectAlternativeName(Type type, String value) {
+ this.type = type;
+ this.value = value;
+ }
+
+ SubjectAlternativeName(GeneralName bcGeneralName) {
+ this.type = Type.fromTag(bcGeneralName.getTagNo());
+ this.value = getValue(bcGeneralName);
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ GeneralName toGeneralName() {
+ return new GeneralName(type.tag, value);
+ }
+
+ static List<SubjectAlternativeName> fromGeneralNames(GeneralNames generalNames) {
+ return Arrays.stream(generalNames.getNames()).map(SubjectAlternativeName::new).collect(toList());
+ }
+
+ private String getValue(GeneralName bcGeneralName) {
+ ASN1Encodable name = bcGeneralName.getName();
+ switch (bcGeneralName.getTagNo()) {
+ case GeneralName.rfc822Name:
+ case GeneralName.dNSName:
+ case GeneralName.uniformResourceIdentifier:
+ return DERIA5String.getInstance(name).getString();
+ case GeneralName.directoryName:
+ return X500Name.getInstance(name).toString();
+ default:
+ return name.toString();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SubjectAlternativeName{" +
+ "type=" + type +
+ ", value='" + value + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SubjectAlternativeName that = (SubjectAlternativeName) o;
+ return type == that.type &&
+ Objects.equals(value, that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, value);
+ }
+
+ public enum Type {
+ OTHER_NAME(0),
+ RFC822_NAME(1),
+ DNS_NAME(2),
+ X400_ADDRESS(3),
+ DIRECTORY_NAME(4),
+ EDI_PARITY_NAME(5),
+ UNIFORM_RESOURCE_IDENTIFIER(6),
+ IP_ADDRESS(7),
+ REGISTERED_ID(8);
+
+ final int tag;
+
+ Type(int tag) {
+ this.tag = tag;
+ }
+
+ public static Type fromTag(int tag) {
+ return Arrays.stream(Type.values())
+ .filter(type -> type.tag == tag)
+ .findAny()
+ .orElseThrow(() -> new IllegalArgumentException("Invalid tag: " + tag));
+ }
+
+ public int getTag() {
+ return tag;
+ }
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/X509CertificateBuilder.java b/security-utils/src/main/java/com/yahoo/security/X509CertificateBuilder.java
new file mode 100644
index 00000000000..54d7d39253e
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/X509CertificateBuilder.java
@@ -0,0 +1,167 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.pkcs.PKCS10CertificationRequest;
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.sql.Date;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME;
+
+
+/**
+ * @author bjorncs
+ */
+public class X509CertificateBuilder {
+
+ private final BigInteger serialNumber;
+ private final SignatureAlgorithm signingAlgorithm;
+ private final PrivateKey caPrivateKey;
+ private final Instant notBefore;
+ private final Instant notAfter;
+ private final List<SubjectAlternativeName> subjectAlternativeNames = new ArrayList<>();
+ private final X500Principal issuer;
+ private final X500Principal subject;
+ private final PublicKey certPublicKey;
+ private BasicConstraintsExtension basicConstraintsExtension;
+
+ private X509CertificateBuilder(X500Principal issuer,
+ X500Principal subject,
+ Instant notBefore,
+ Instant notAfter,
+ PublicKey certPublicKey,
+ PrivateKey caPrivateKey,
+ SignatureAlgorithm signingAlgorithm,
+ BigInteger serialNumber) {
+ this.issuer = issuer;
+ this.subject = subject;
+ this.notBefore = notBefore;
+ this.notAfter = notAfter;
+ this.certPublicKey = certPublicKey;
+ this.caPrivateKey = caPrivateKey;
+ this.signingAlgorithm = signingAlgorithm;
+ this.serialNumber = serialNumber;
+ }
+
+ public static X509CertificateBuilder fromCsr(Pkcs10Csr csr,
+ X500Principal caIssuer,
+ Instant notBefore,
+ Instant notAfter,
+ PrivateKey caPrivateKey,
+ SignatureAlgorithm signingAlgorithm,
+ BigInteger serialNumber) {
+ try {
+ PKCS10CertificationRequest bcCsr = csr.getBcCsr();
+ PublicKey publicKey = new JcaPKCS10CertificationRequest(bcCsr)
+ .setProvider(BouncyCastleProviderHolder.getInstance())
+ .getPublicKey();
+ return new X509CertificateBuilder(caIssuer,
+ new X500Principal(bcCsr.getSubject().getEncoded()),
+ notBefore,
+ notAfter,
+ publicKey,
+ caPrivateKey,
+ signingAlgorithm,
+ serialNumber);
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public static X509CertificateBuilder fromKeypair(KeyPair keyPair,
+ X500Principal subject,
+ Instant notBefore,
+ Instant notAfter,
+ SignatureAlgorithm signingAlgorithm,
+ BigInteger serialNumber) {
+ return new X509CertificateBuilder(subject,
+ subject,
+ notBefore,
+ notAfter,
+ keyPair.getPublic(),
+ keyPair.getPrivate(),
+ signingAlgorithm,
+ serialNumber);
+ }
+
+ /**
+ * @return generates a cryptographically secure positive serial number up to 128 bits
+ */
+ public static BigInteger generateRandomSerialNumber() {
+ return new BigInteger(128, new SecureRandom());
+ }
+
+ public X509CertificateBuilder addSubjectAlternativeName(String dnsName) {
+ this.subjectAlternativeNames.add(new SubjectAlternativeName(DNS_NAME, dnsName));
+ return this;
+ }
+
+ public X509CertificateBuilder addSubjectAlternativeName(SubjectAlternativeName san) {
+ this.subjectAlternativeNames.add(san);
+ return this;
+ }
+
+ public X509CertificateBuilder setBasicConstraints(boolean isCritical, boolean isCertAuthorityCertificate) {
+ this.basicConstraintsExtension = new BasicConstraintsExtension(isCritical, isCertAuthorityCertificate);
+ return this;
+ }
+
+ public X509CertificateBuilder setIsCertAuthority(boolean isCertAuthority) {
+ return setBasicConstraints(true, isCertAuthority);
+ }
+
+ public X509Certificate build() {
+ try {
+ JcaX509v3CertificateBuilder jcaCertBuilder = new JcaX509v3CertificateBuilder(
+ issuer, serialNumber, Date.from(notBefore), Date.from(notAfter), subject, certPublicKey);
+ if (basicConstraintsExtension != null) {
+ jcaCertBuilder.addExtension(
+ Extension.basicConstraints,
+ basicConstraintsExtension.isCritical,
+ new BasicConstraints(basicConstraintsExtension.isCertAuthorityCertificate));
+ }
+ if (!subjectAlternativeNames.isEmpty()) {
+ GeneralNames generalNames = new GeneralNames(
+ subjectAlternativeNames.stream()
+ .map(SubjectAlternativeName::toGeneralName)
+ .toArray(GeneralName[]::new));
+ jcaCertBuilder.addExtension(Extension.subjectAlternativeName, false, generalNames);
+ }
+ ContentSigner contentSigner = new JcaContentSignerBuilder(signingAlgorithm.getAlgorithmName())
+ .setProvider(BouncyCastleProviderHolder.getInstance())
+ .build(caPrivateKey);
+ return new JcaX509CertificateConverter()
+ .setProvider(BouncyCastleProviderHolder.getInstance())
+ .getCertificate(jcaCertBuilder.build(contentSigner));
+ } catch (OperatorException | GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java b/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java
new file mode 100644
index 00000000000..33bd750bac5
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java
@@ -0,0 +1,136 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1OctetString;
+import org.bouncycastle.asn1.ASN1Primitive;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.util.io.pem.PemObject;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapName;
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.UncheckedIOException;
+import java.security.GeneralSecurityException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static com.yahoo.security.Extension.SUBJECT_ALTERNATIVE_NAMES;
+import static java.util.stream.Collectors.toList;
+
+/**
+ * @author bjorncs
+ */
+public class X509CertificateUtils {
+
+ private X509CertificateUtils() {}
+
+ public static X509Certificate fromPem(String pem) {
+ try (PEMParser parser = new PEMParser(new StringReader(pem))) {
+ return toX509Certificate(parser.readObject());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } catch (CertificateException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static List<X509Certificate> certificateListFromPem(String pem) {
+ try (PEMParser parser = new PEMParser(new StringReader(pem))) {
+ List<X509Certificate> list = new ArrayList<>();
+ Object pemObject;
+ while ((pemObject = parser.readObject()) != null) {
+ list.add(toX509Certificate(pemObject));
+ }
+ return list;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } catch (CertificateException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static X509Certificate toX509Certificate(Object pemObject) throws CertificateException {
+ if (pemObject instanceof X509Certificate) {
+ return (X509Certificate) pemObject;
+ }
+ if (pemObject instanceof X509CertificateHolder) {
+ return new JcaX509CertificateConverter()
+ .setProvider(BouncyCastleProviderHolder.getInstance())
+ .getCertificate((X509CertificateHolder) pemObject);
+ }
+ throw new IllegalArgumentException("Invalid type of PEM object: " + pemObject);
+ }
+
+ public static String toPem(X509Certificate certificate) {
+ try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
+ pemWriter.writeObject(new PemObject("CERTIFICATE", certificate.getEncoded()));
+ pemWriter.flush();
+ return stringWriter.toString();
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public static String toPem(List<X509Certificate> certificates) {
+ try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
+ for (X509Certificate certificate : certificates) {
+ pemWriter.writeObject(new PemObject("CERTIFICATE", certificate.getEncoded()));
+ }
+ pemWriter.flush();
+ return stringWriter.toString();
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public static List<String> getSubjectCommonNames(X509Certificate certificate) {
+ return getCommonNames(certificate.getSubjectX500Principal());
+ }
+
+ public static List<String> getIssuerCommonNames(X509Certificate certificate) {
+ return getCommonNames(certificate.getIssuerX500Principal());
+ }
+
+ public static List<String> getCommonNames(X500Principal subject) {
+ try {
+ String subjectPrincipal = subject.getName();
+ return new LdapName(subjectPrincipal).getRdns().stream()
+ .filter(rdn -> rdn.getType().equalsIgnoreCase("cn"))
+ .map(rdn -> rdn.getValue().toString())
+ .collect(toList());
+ } catch (NamingException e) {
+ throw new IllegalArgumentException("Invalid CN: " + e, e);
+ }
+
+ }
+
+ public static List<SubjectAlternativeName> getSubjectAlternativeNames(X509Certificate certificate) {
+ try {
+ byte[] extensionValue = certificate.getExtensionValue(SUBJECT_ALTERNATIVE_NAMES.getOId());
+ if (extensionValue == null) return Collections.emptyList();
+ ASN1Encodable asn1Encodable = ASN1Primitive.fromByteArray(extensionValue);
+ if (asn1Encodable instanceof ASN1OctetString) {
+ asn1Encodable = ASN1Primitive.fromByteArray(((ASN1OctetString) asn1Encodable).getOctets());
+ }
+ GeneralNames names = GeneralNames.getInstance(asn1Encodable);
+ return SubjectAlternativeName.fromGeneralNames(names);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/package-info.java b/security-utils/src/main/java/com/yahoo/security/package-info.java
new file mode 100644
index 00000000000..10a4c9c0e0d
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/package-info.java
@@ -0,0 +1,9 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author bjorncs
+ */
+
+@ExportPackage
+package com.yahoo.security;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java
new file mode 100644
index 00000000000..f0d1edd6889
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java
@@ -0,0 +1,90 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Generic TLS configuration for Vespa
+ *
+ * @author bjorncs
+ */
+public class TransportSecurityOptions {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ private final Path privateKeyFile;
+ private final Path certificatesFile;
+ private final Path caCertificatesFile;
+
+ public TransportSecurityOptions(String privateKeyFile, String certificatesFile, String caCertificatesFile) {
+ this(Paths.get(privateKeyFile), Paths.get(certificatesFile), Paths.get(caCertificatesFile));
+ }
+
+ public TransportSecurityOptions(Path privateKeyFile, Path certificatesFile, Path caCertificatesFile) {
+ this.privateKeyFile = privateKeyFile;
+ this.certificatesFile = certificatesFile;
+ this.caCertificatesFile = caCertificatesFile;
+ }
+
+ public Path getPrivateKeyFile() {
+ return privateKeyFile;
+ }
+
+ public Path getCertificatesFile() {
+ return certificatesFile;
+ }
+
+ public Path getCaCertificatesFile() {
+ return caCertificatesFile;
+ }
+
+ public static TransportSecurityOptions fromJsonFile(Path file) {
+ try {
+ JsonNode root = mapper.readTree(file.toFile());
+ JsonNode filesNode = getField(root, "files");
+ String privateKeyFile = getField(filesNode, "private-key").asText();
+ String certificatesFile = getField(filesNode, "certificates").asText();
+ String caCertificatesFile = getField(filesNode, "ca-certificates").asText();
+ return new TransportSecurityOptions(privateKeyFile, certificatesFile, caCertificatesFile);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static JsonNode getField(JsonNode root, String fieldName) {
+ return Optional.ofNullable(root.get(fieldName))
+ .orElseThrow(() -> new IllegalArgumentException(String.format("'%s' field missing", fieldName)));
+ }
+
+ @Override
+ public String toString() {
+ return "TransportSecurityOptions{" +
+ "privateKeyFile=" + privateKeyFile +
+ ", certificatesFile=" + certificatesFile +
+ ", caCertificatesFile=" + caCertificatesFile +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TransportSecurityOptions that = (TransportSecurityOptions) o;
+ return Objects.equals(privateKeyFile, that.privateKeyFile) &&
+ Objects.equals(certificatesFile, that.certificatesFile) &&
+ Objects.equals(caCertificatesFile, that.caCertificatesFile);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(privateKeyFile, certificatesFile, caCertificatesFile);
+ }
+} \ No newline at end of file
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java
new file mode 100644
index 00000000000..5595d33a9b5
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java
@@ -0,0 +1,66 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * Utility class for retrieving {@link TransportSecurityOptions} from the system.
+ *
+ * @author bjorncs
+ */
+public class TransportSecurityUtils {
+
+ public static final String CONFIG_FILE_ENVIRONMENT_VARIABLE = "VESPA_TLS_CONFIG_FILE";
+ public static final String INSECURE_MIXED_MODE_ENVIRONMENT_VARIABLE = "VESPA_TLS_INSECURE_MIXED_MODE";
+
+ public enum MixedMode {
+ PLAINTEXT_CLIENT_MIXED_SERVER("plaintext_client_mixed_server"),
+ TLS_CLIENT_MIXED_SERVER("tls_client_mixed_server");
+
+ final String configValue;
+
+ MixedMode(String configValue) {
+ this.configValue = configValue;
+ }
+
+ static MixedMode fromConfigValue(String configValue) {
+ return Arrays.stream(values())
+ .filter(v -> v.configValue.equals(configValue))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("Unknown value: " + configValue));
+ }
+ }
+
+ private TransportSecurityUtils() {}
+
+ public static boolean isTransportSecurityEnabled() {
+ return getConfigFile().isPresent();
+ }
+
+ public static boolean isInsecureMixedModeEnabled() {
+ return getInsecureMixedMode().isPresent();
+ }
+
+ public static Optional<MixedMode> getInsecureMixedMode() {
+ if (!isTransportSecurityEnabled()) return Optional.empty();
+ return getEnvironmentVariable(INSECURE_MIXED_MODE_ENVIRONMENT_VARIABLE)
+ .map(MixedMode::fromConfigValue);
+ }
+
+ public static Optional<Path> getConfigFile() {
+ return getEnvironmentVariable(CONFIG_FILE_ENVIRONMENT_VARIABLE).map(Paths::get);
+ }
+
+ public static Optional<TransportSecurityOptions> getOptions() {
+ return getConfigFile()
+ .map(TransportSecurityOptions::fromJsonFile);
+ }
+
+ private static Optional<String> getEnvironmentVariable(String environmentVariable) {
+ return Optional.ofNullable(System.getenv(environmentVariable))
+ .filter(var -> !var.isEmpty());
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/package-info.java b/security-utils/src/main/java/com/yahoo/security/tls/package-info.java
new file mode 100644
index 00000000000..b5668182f14
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author bjorncs
+ */
+@ExportPackage
+package com.yahoo.security.tls;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/security-utils/src/test/java/com/yahoo/security/KeyStoreBuilderTest.java b/security-utils/src/test/java/com/yahoo/security/KeyStoreBuilderTest.java
new file mode 100644
index 00000000000..06ea5d963a3
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/KeyStoreBuilderTest.java
@@ -0,0 +1,55 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+
+import static com.yahoo.security.TestUtils.createCertificate;
+import static com.yahoo.security.TestUtils.createKeystoreFile;
+
+
+/**
+ * @author bjorncs
+ */
+public class KeyStoreBuilderTest {
+
+ private static final char[] PASSWORD = new char[0];
+
+ @Rule
+ public TemporaryFolder tempDirectory = new TemporaryFolder();
+
+ @Test
+ public void can_create_jks_keystore_from_privatekey_and_certificate() throws Exception {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ X509Certificate certificate = createCertificate(keyPair);
+ KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .withKeyEntry("key", keyPair.getPrivate(), certificate)
+ .build();
+ }
+
+ @Test
+ public void can_build_jks_keystore_from_file() throws Exception {
+ Path keystoreFile = tempDirectory.newFile().toPath();
+ createKeystoreFile(keystoreFile, KeyStoreType.JKS, PASSWORD);
+
+ KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .fromFile(keystoreFile, PASSWORD)
+ .build();
+ }
+
+ @Test
+ public void can_build_pcks12_keystore_from_file() throws Exception {
+ Path keystoreFile = tempDirectory.newFile().toPath();
+ createKeystoreFile(keystoreFile, KeyStoreType.PKCS12, PASSWORD);
+
+ KeyStoreBuilder.withType(KeyStoreType.PKCS12)
+ .fromFile(keystoreFile, PASSWORD)
+ .build();
+ }
+
+} \ No newline at end of file
diff --git a/security-utils/src/test/java/com/yahoo/security/KeyUtilsTest.java b/security-utils/src/test/java/com/yahoo/security/KeyUtilsTest.java
new file mode 100644
index 00000000000..5e786654d7c
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/KeyUtilsTest.java
@@ -0,0 +1,54 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.junit.Test;
+
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author bjorncs
+ */
+public class KeyUtilsTest {
+
+ @Test
+ public void can_extract_public_key_from_rsa_private() {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA);
+ PublicKey publicKey = KeyUtils.extractPublicKey(keyPair.getPrivate());
+ assertNotNull(publicKey);
+ }
+
+ @Test
+ public void can_extract_public_key_from_ecdsa_private() {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
+ PublicKey publicKey = KeyUtils.extractPublicKey(keyPair.getPrivate());
+ assertNotNull(publicKey);
+ }
+
+ @Test
+ public void can_serialize_and_deserialize_rsa_privatekey_using_pem_format() {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA);
+ String pem = KeyUtils.toPem(keyPair.getPrivate());
+ assertThat(pem, containsString("BEGIN RSA PRIVATE KEY"));
+ assertThat(pem, containsString("END RSA PRIVATE KEY"));
+ PrivateKey deserializedKey = KeyUtils.fromPemEncodedPrivateKey(pem);
+ assertEquals(keyPair.getPrivate(), deserializedKey);
+ }
+
+ @Test
+ public void can_serialize_and_deserialize_ec_privatekey_using_pem_format() {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
+ String pem = KeyUtils.toPem(keyPair.getPrivate());
+ assertThat(pem, containsString("BEGIN EC PRIVATE KEY"));
+ assertThat(pem, containsString("END EC PRIVATE KEY"));
+ PrivateKey deserializedKey = KeyUtils.fromPemEncodedPrivateKey(pem);
+ assertEquals(keyPair.getPrivate(), deserializedKey);
+ }
+
+} \ No newline at end of file
diff --git a/security-utils/src/test/java/com/yahoo/security/Pkcs10CsrBuilderTest.java b/security-utils/src/test/java/com/yahoo/security/Pkcs10CsrBuilderTest.java
new file mode 100644
index 00000000000..d51203a5cb2
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/Pkcs10CsrBuilderTest.java
@@ -0,0 +1,27 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.junit.Test;
+
+import javax.security.auth.x500.X500Principal;
+import java.security.KeyPair;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bjorncs
+ */
+public class Pkcs10CsrBuilderTest {
+
+ @Test
+ public void can_build_csr_with_sans() {
+ X500Principal subject = new X500Principal("CN=subject");
+ KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA512_WITH_ECDSA)
+ .addSubjectAlternativeName("san1.com")
+ .addSubjectAlternativeName("san2.com")
+ .build();
+ assertEquals(subject, csr.getSubject());
+ }
+
+} \ No newline at end of file
diff --git a/security-utils/src/test/java/com/yahoo/security/Pkcs10CsrTest.java b/security-utils/src/test/java/com/yahoo/security/Pkcs10CsrTest.java
new file mode 100644
index 00000000000..cc1f6cc6a14
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/Pkcs10CsrTest.java
@@ -0,0 +1,57 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.junit.Test;
+
+import javax.security.auth.x500.X500Principal;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bjorncs
+ */
+public class Pkcs10CsrTest {
+
+ @Test
+ public void can_read_subject_alternative_names() {
+ X500Principal subject = new X500Principal("CN=subject");
+ KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ SubjectAlternativeName san1 = new SubjectAlternativeName(DNS_NAME, "san1.com");
+ SubjectAlternativeName san2 = new SubjectAlternativeName(DNS_NAME, "san2.com");
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA512_WITH_ECDSA)
+ .addSubjectAlternativeName(san1)
+ .addSubjectAlternativeName(san2)
+ .build();
+ assertEquals(Arrays.asList(san1, san2), csr.getSubjectAlternativeNames());
+ }
+
+ @Test
+ public void can_read_basic_constraints() {
+ X500Principal subject = new X500Principal("CN=subject");
+ KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA512_WITH_ECDSA)
+ .setBasicConstraints(true, true)
+ .build();
+ assertTrue(csr.getBasicConstraints().isPresent());
+ assertTrue(csr.getBasicConstraints().get());
+ }
+
+ @Test
+ public void can_read_extensions() {
+ X500Principal subject = new X500Principal("CN=subject");
+ KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA512_WITH_ECDSA)
+ .addSubjectAlternativeName("san")
+ .setBasicConstraints(true, true)
+ .build();
+ List<String> expected = Arrays.asList(Extension.BASIC_CONSTRAINTS.getOId(), Extension.SUBJECT_ALTERNATIVE_NAMES.getOId());
+ List<String> actual = csr.getExtensionOIds();
+ assertEquals(expected, actual);
+ }
+
+} \ No newline at end of file
diff --git a/security-utils/src/test/java/com/yahoo/security/Pkcs10CsrUtilsTest.java b/security-utils/src/test/java/com/yahoo/security/Pkcs10CsrUtilsTest.java
new file mode 100644
index 00000000000..04d35a537bb
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/Pkcs10CsrUtilsTest.java
@@ -0,0 +1,30 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.junit.Test;
+
+import javax.security.auth.x500.X500Principal;
+import java.security.KeyPair;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author bjorncs
+ */
+public class Pkcs10CsrUtilsTest {
+
+ @Test
+ public void can_deserialize_serialized_pem_csr() {
+ X500Principal subject = new X500Principal("CN=subject");
+ KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA512_WITH_ECDSA).build();
+ String pem = Pkcs10CsrUtils.toPem(csr);
+ Pkcs10Csr deserializedCsr = Pkcs10CsrUtils.fromPem(pem);
+ assertThat(pem, containsString("BEGIN CERTIFICATE REQUEST"));
+ assertThat(pem, containsString("END CERTIFICATE REQUEST"));
+ assertEquals(subject, deserializedCsr.getSubject());
+ }
+
+} \ No newline at end of file
diff --git a/security-utils/src/test/java/com/yahoo/security/SslContextBuilderTest.java b/security-utils/src/test/java/com/yahoo/security/SslContextBuilderTest.java
new file mode 100644
index 00000000000..cc269a4ef43
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/SslContextBuilderTest.java
@@ -0,0 +1,77 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+
+import static com.yahoo.security.TestUtils.createCertificate;
+import static com.yahoo.security.TestUtils.createKeystore;
+import static com.yahoo.security.TestUtils.createKeystoreFile;
+
+/**
+ * @author bjorncs
+ */
+public class SslContextBuilderTest {
+
+ private static final char[] PASSWORD = new char[0];
+
+ @Rule
+ public TemporaryFolder tempDirectory = new TemporaryFolder();
+
+ @Test
+ public void can_build_sslcontext_with_truststore_only() throws Exception {
+ new SslContextBuilder()
+ .withTrustStore(createKeystore(KeyStoreType.JKS, PASSWORD))
+ .build();
+ }
+
+ @Test
+ public void can_build_sslcontext_with_keystore_only() throws Exception {
+ new SslContextBuilder()
+ .withKeyStore(createKeystore(KeyStoreType.JKS, PASSWORD), PASSWORD)
+ .build();
+ }
+
+ @Test
+ public void can_build_sslcontext_with_truststore_and_keystore() throws Exception {
+ new SslContextBuilder()
+ .withKeyStore(createKeystore(KeyStoreType.JKS, PASSWORD), PASSWORD)
+ .withTrustStore(createKeystore(KeyStoreType.JKS, PASSWORD))
+ .build();
+ }
+
+ @Test
+ public void can_build_sslcontext_with_keystore_from_private_key_and_certificate() throws Exception {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ X509Certificate certificate = createCertificate(keyPair);
+ new SslContextBuilder()
+ .withKeyStore(keyPair.getPrivate(), certificate)
+ .build();
+ }
+
+ @Test
+ public void can_build_sslcontext_with_jks_keystore_from_file() throws Exception {
+ Path keystoreFile = tempDirectory.newFile().toPath();
+ createKeystoreFile(keystoreFile, KeyStoreType.JKS, PASSWORD);
+
+ new SslContextBuilder()
+ .withKeyStore(keystoreFile, PASSWORD, KeyStoreType.JKS)
+ .build();
+ }
+
+ @Test
+ public void can_build_sslcontext_with_pcks12_keystore_from_file() throws Exception {
+ Path keystoreFile = tempDirectory.newFile().toPath();
+ createKeystoreFile(keystoreFile, KeyStoreType.PKCS12, PASSWORD);
+
+ new SslContextBuilder()
+ .withKeyStore(keystoreFile, PASSWORD, KeyStoreType.PKCS12)
+ .build();
+ }
+
+}
diff --git a/security-utils/src/test/java/com/yahoo/security/TestUtils.java b/security-utils/src/test/java/com/yahoo/security/TestUtils.java
new file mode 100644
index 00000000000..fcfcfb2b761
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/TestUtils.java
@@ -0,0 +1,42 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import javax.security.auth.x500.X500Principal;
+import java.math.BigInteger;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import static com.yahoo.security.KeyStoreUtils.writeKeyStoreToFile;
+
+
+/**
+ * @author bjorncs
+ */
+class TestUtils {
+
+ static KeyStore createKeystore(KeyStoreType type, char[] password) {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ return KeyStoreBuilder.withType(type)
+ .withKeyEntry("entry-name", keyPair.getPrivate(), password, createCertificate(keyPair))
+ .build();
+ }
+
+ static X509Certificate createCertificate(KeyPair keyPair) {
+ return createCertificate(keyPair, new X500Principal("CN=mysubject"));
+ }
+
+ static X509Certificate createCertificate(KeyPair keyPair, X500Principal subject) {
+ return X509CertificateBuilder
+ .fromKeypair(
+ keyPair, subject, Instant.now(), Instant.now().plus(1, ChronoUnit.DAYS), SignatureAlgorithm.SHA512_WITH_ECDSA, BigInteger.valueOf(1))
+ .build();
+ }
+
+ static void createKeystoreFile(Path file, KeyStoreType type, char[] password) {
+ writeKeyStoreToFile(createKeystore(type, password), file, password);
+ }
+}
diff --git a/security-utils/src/test/java/com/yahoo/security/X509CertificateBuilderTest.java b/security-utils/src/test/java/com/yahoo/security/X509CertificateBuilderTest.java
new file mode 100644
index 00000000000..7e6d343b570
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/X509CertificateBuilderTest.java
@@ -0,0 +1,83 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import javax.security.auth.x500.X500Principal;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Collection;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bjorncs
+ */
+@RunWith(Parameterized.class)
+public class X509CertificateBuilderTest {
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][] {
+ {KeyAlgorithm.RSA, 2048, SignatureAlgorithm.SHA512_WITH_RSA},
+ {KeyAlgorithm.EC, 256, SignatureAlgorithm.SHA512_WITH_ECDSA}});
+ }
+
+ private final KeyAlgorithm keyAlgorithm;
+ private final int keySize;
+ private final SignatureAlgorithm signatureAlgorithm;
+
+ public X509CertificateBuilderTest(KeyAlgorithm keyAlgorithm,
+ int keySize,
+ SignatureAlgorithm signatureAlgorithm) {
+ this.keyAlgorithm = keyAlgorithm;
+ this.keySize = keySize;
+ this.signatureAlgorithm = signatureAlgorithm;
+ }
+
+ @Test
+ public void can_build_self_signed_certificate() {
+ KeyPair keyPair = KeyUtils.generateKeypair(keyAlgorithm, keySize);
+ X500Principal subject = new X500Principal("CN=myservice");
+ X509Certificate cert =
+ X509CertificateBuilder.fromKeypair(
+ keyPair,
+ subject,
+ Instant.now(),
+ Instant.now().plus(1, ChronoUnit.DAYS),
+ signatureAlgorithm,
+ BigInteger.valueOf(1))
+ .setBasicConstraints(true, true)
+ .build();
+ assertEquals(subject, cert.getSubjectX500Principal());
+ }
+
+ @Test
+ public void can_build_certificate_from_csr() {
+ X500Principal subject = new X500Principal("CN=subject");
+ X500Principal issuer = new X500Principal("CN=issuer");
+ KeyPair csrKeypair = KeyUtils.generateKeypair(keyAlgorithm, keySize);
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, csrKeypair, signatureAlgorithm).build();
+ KeyPair caKeypair = KeyUtils.generateKeypair(keyAlgorithm, keySize);
+ X509Certificate cert = X509CertificateBuilder
+ .fromCsr(
+ csr,
+ issuer,
+ Instant.now(),
+ Instant.now().plus(1, ChronoUnit.DAYS),
+ caKeypair.getPrivate(),
+ signatureAlgorithm,
+ BigInteger.valueOf(1))
+ .addSubjectAlternativeName("subject1.alt")
+ .addSubjectAlternativeName("subject2.alt")
+ .build();
+ assertEquals(subject, cert.getSubjectX500Principal());
+ }
+
+} \ No newline at end of file
diff --git a/security-utils/src/test/java/com/yahoo/security/X509CertificateUtilsTest.java b/security-utils/src/test/java/com/yahoo/security/X509CertificateUtilsTest.java
new file mode 100644
index 00000000000..76a93028efe
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/X509CertificateUtilsTest.java
@@ -0,0 +1,74 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.junit.Test;
+
+import javax.security.auth.x500.X500Principal;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author bjorncs
+ */
+public class X509CertificateUtilsTest {
+ @Test
+ public void can_deserialize_serialized_pem_certificate() {
+ KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ X500Principal subject = new X500Principal("CN=myservice");
+ X509Certificate cert = TestUtils.createCertificate(keypair, subject);
+ assertEquals(subject, cert.getSubjectX500Principal());
+ String pem = X509CertificateUtils.toPem(cert);
+ assertThat(pem, containsString("BEGIN CERTIFICATE"));
+ assertThat(pem, containsString("END CERTIFICATE"));
+ X509Certificate deserializedCert = X509CertificateUtils.fromPem(pem);
+ assertEquals(subject, deserializedCert.getSubjectX500Principal());
+ }
+
+ @Test
+ public void can_deserialize_serialized_pem_certificate_list() {
+ KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ X500Principal subject1 = new X500Principal("CN=myservice1");
+ X509Certificate cert1 = TestUtils.createCertificate(keypair, subject1);
+ X500Principal subject2 = new X500Principal("CN=myservice2");
+ X509Certificate cert2 = TestUtils.createCertificate(keypair, subject2);
+ List<X509Certificate> certificateList = Arrays.asList(cert1, cert2);
+ String pem = X509CertificateUtils.toPem(certificateList);
+ List<X509Certificate> deserializedCertificateList = X509CertificateUtils.certificateListFromPem(pem);
+ assertEquals(2, certificateList.size());
+ assertEquals(subject1, deserializedCertificateList.get(0).getSubjectX500Principal());
+ assertEquals(subject2, deserializedCertificateList.get(1).getSubjectX500Principal());
+ }
+
+ @Test
+ public void can_list_subject_alternative_names() {
+ KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ X500Principal subject = new X500Principal("CN=myservice");
+ SubjectAlternativeName san = new SubjectAlternativeName(DNS_NAME, "dns-san");
+ X509Certificate cert = X509CertificateBuilder
+ .fromKeypair(
+ keypair,
+ subject,
+ Instant.now(),
+ Instant.now().plus(1, ChronoUnit.DAYS),
+ SignatureAlgorithm.SHA512_WITH_ECDSA,
+ BigInteger.valueOf(1))
+ .addSubjectAlternativeName(san)
+ .build();
+
+ List<SubjectAlternativeName> sans = X509CertificateUtils.getSubjectAlternativeNames(cert);
+ assertThat(sans.size(), is(1));
+ assertThat(sans.get(0), equalTo(san));
+ }
+} \ No newline at end of file
diff --git a/security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsTest.java b/security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsTest.java
new file mode 100644
index 00000000000..f311651cab0
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsTest.java
@@ -0,0 +1,25 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import org.junit.Test;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author bjorncs
+ */
+public class TransportSecurityOptionsTest {
+
+ private static final Path TEST_CONFIG_FILE = Paths.get("src/test/resources/transport-security-options.json");
+
+ @Test
+ public void can_read_options_from_json_file() {
+ TransportSecurityOptions expectedOptions = new TransportSecurityOptions("myhost.key", "certs.pem", "my_cas.pem");
+ TransportSecurityOptions actualOptions = TransportSecurityOptions.fromJsonFile(TEST_CONFIG_FILE);
+ assertEquals(expectedOptions, actualOptions);
+ }
+
+}
diff --git a/security-utils/src/test/resources/transport-security-options.json b/security-utils/src/test/resources/transport-security-options.json
new file mode 100644
index 00000000000..0506c130722
--- /dev/null
+++ b/security-utils/src/test/resources/transport-security-options.json
@@ -0,0 +1,7 @@
+{
+ "files": {
+ "private-key": "myhost.key",
+ "ca-certificates": "my_cas.pem",
+ "certificates": "certs.pem"
+ }
+} \ No newline at end of file