summaryrefslogtreecommitdiffstats
path: root/vespajlib
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@oath.com>2018-08-30 16:35:55 +0200
committerBjørn Christian Seime <bjorncs@oath.com>2018-08-30 17:53:09 +0200
commit410fd34a61b062a2e8fb57752dbba16cb1d1471f (patch)
tree087aff106d3c928c3c69ebf876b354eed783b8a1 /vespajlib
parentc3ad87b12928fb61e456cbf3e0c5b61221750816 (diff)
Copy security related utility classes from vespa-athenz to vespajlib
- The clones in vespa-athenz will be removed in the future
Diffstat (limited to 'vespajlib')
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/BasicConstraintsExtension.java14
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/BouncyCastleProviderHolder.java14
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/Extension.java22
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/KeyAlgorithm.java20
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/KeyStoreBuilder.java121
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/KeyStoreType.java23
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/KeyStoreUtils.java34
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/KeyUtils.java120
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/Pkcs10Csr.java71
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/Pkcs10CsrBuilder.java101
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/Pkcs10CsrUtils.java38
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/SignatureAlgorithm.java20
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/SslContextBuilder.java107
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/SubjectAlternativeName.java114
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/X509CertificateBuilder.java153
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/X509CertificateUtils.java136
-rw-r--r--vespajlib/src/main/java/com/yahoo/security/package-info.java9
-rw-r--r--vespajlib/src/test/java/com/yahoo/security/KeyStoreBuilderTest.java55
-rw-r--r--vespajlib/src/test/java/com/yahoo/security/KeyUtilsTest.java37
-rw-r--r--vespajlib/src/test/java/com/yahoo/security/Pkcs10CsrBuilderTest.java27
-rw-r--r--vespajlib/src/test/java/com/yahoo/security/Pkcs10CsrTest.java57
-rw-r--r--vespajlib/src/test/java/com/yahoo/security/Pkcs10CsrUtilsTest.java30
-rw-r--r--vespajlib/src/test/java/com/yahoo/security/SslContextBuilderTest.java77
-rw-r--r--vespajlib/src/test/java/com/yahoo/security/TestUtils.java41
-rw-r--r--vespajlib/src/test/java/com/yahoo/security/X509CertificateBuilderTest.java59
-rw-r--r--vespajlib/src/test/java/com/yahoo/security/X509CertificateUtilsTest.java73
26 files changed, 1573 insertions, 0 deletions
diff --git a/vespajlib/src/main/java/com/yahoo/security/BasicConstraintsExtension.java b/vespajlib/src/main/java/com/yahoo/security/BasicConstraintsExtension.java
new file mode 100644
index 00000000000..d3c08ba27d0
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/main/java/com/yahoo/security/BouncyCastleProviderHolder.java b/vespajlib/src/main/java/com/yahoo/security/BouncyCastleProviderHolder.java
new file mode 100644
index 00000000000..48a23a1fe7e
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/main/java/com/yahoo/security/Extension.java b/vespajlib/src/main/java/com/yahoo/security/Extension.java
new file mode 100644
index 00000000000..7145321d844
--- /dev/null
+++ b/vespajlib/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_CONSTRAINS(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/vespajlib/src/main/java/com/yahoo/security/KeyAlgorithm.java b/vespajlib/src/main/java/com/yahoo/security/KeyAlgorithm.java
new file mode 100644
index 00000000000..3218f81f0d6
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/main/java/com/yahoo/security/KeyStoreBuilder.java b/vespajlib/src/main/java/com/yahoo/security/KeyStoreBuilder.java
new file mode 100644
index 00000000000..2160fbf6455
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/main/java/com/yahoo/security/KeyStoreType.java b/vespajlib/src/main/java/com/yahoo/security/KeyStoreType.java
new file mode 100644
index 00000000000..7fb8df35286
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/main/java/com/yahoo/security/KeyStoreUtils.java b/vespajlib/src/main/java/com/yahoo/security/KeyStoreUtils.java
new file mode 100644
index 00000000000..f0c4d99bf69
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/main/java/com/yahoo/security/KeyUtils.java b/vespajlib/src/main/java/com/yahoo/security/KeyUtils.java
new file mode 100644
index 00000000000..3f6070bc86f
--- /dev/null
+++ b/vespajlib/src/main/java/com/yahoo/security/KeyUtils.java
@@ -0,0 +1,120 @@
+// 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());
+ 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();
+ 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)) {
+ // Note: Encoding using PKCS#1 as this is to be read by tools only supporting PKCS#1
+ pemWriter.writeObject(new PemObject("RSA PRIVATE KEY", 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/vespajlib/src/main/java/com/yahoo/security/Pkcs10Csr.java b/vespajlib/src/main/java/com/yahoo/security/Pkcs10Csr.java
new file mode 100644
index 00000000000..e08ee117fcd
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/main/java/com/yahoo/security/Pkcs10CsrBuilder.java b/vespajlib/src/main/java/com/yahoo/security/Pkcs10CsrBuilder.java
new file mode 100644
index 00000000000..49a69b00f26
--- /dev/null
+++ b/vespajlib/src/main/java/com/yahoo/security/Pkcs10CsrBuilder.java
@@ -0,0 +1,101 @@
+// 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.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 Pkcs10Csr build() {
+ try {
+ PKCS10CertificationRequestBuilder requestBuilder =
+ new JcaPKCS10CertificationRequestBuilder(subject, 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/vespajlib/src/main/java/com/yahoo/security/Pkcs10CsrUtils.java b/vespajlib/src/main/java/com/yahoo/security/Pkcs10CsrUtils.java
new file mode 100644
index 00000000000..6f12450528d
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/main/java/com/yahoo/security/SignatureAlgorithm.java b/vespajlib/src/main/java/com/yahoo/security/SignatureAlgorithm.java
new file mode 100644
index 00000000000..552d964bf51
--- /dev/null
+++ b/vespajlib/src/main/java/com/yahoo/security/SignatureAlgorithm.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 SignatureAlgorithm {
+ SHA256_WITH_RSA("SHA256withRSA"),
+ SHA512_WITH_RSA("SHA512withRSA");
+
+ private final String algorithmName;
+
+ SignatureAlgorithm(String algorithmName) {
+ this.algorithmName = algorithmName;
+ }
+
+ public String getAlgorithmName() {
+ return algorithmName;
+ }
+}
diff --git a/vespajlib/src/main/java/com/yahoo/security/SslContextBuilder.java b/vespajlib/src/main/java/com/yahoo/security/SslContextBuilder.java
new file mode 100644
index 00000000000..56db9c59146
--- /dev/null
+++ b/vespajlib/src/main/java/com/yahoo/security/SslContextBuilder.java
@@ -0,0 +1,107 @@
+// 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;
+
+/**
+ * @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 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 certificatePemFile) {
+ this.keyStoreSupplier =
+ () -> {
+ PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(new String(Files.readAllBytes(privateKeyPemFile)));
+ X509Certificate certificate = X509CertificateUtils.fromPem(new String(Files.readAllBytes(certificatePemFile)));
+ return KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .withKeyEntry("default", privateKey, certificate)
+ .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 interface KeyStoreSupplier {
+ KeyStore get() throws IOException, GeneralSecurityException;
+ }
+
+}
diff --git a/vespajlib/src/main/java/com/yahoo/security/SubjectAlternativeName.java b/vespajlib/src/main/java/com/yahoo/security/SubjectAlternativeName.java
new file mode 100644
index 00000000000..29395c75e70
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/main/java/com/yahoo/security/X509CertificateBuilder.java b/vespajlib/src/main/java/com/yahoo/security/X509CertificateBuilder.java
new file mode 100644
index 00000000000..5b2cc1127a9
--- /dev/null
+++ b/vespajlib/src/main/java/com/yahoo/security/X509CertificateBuilder.java
@@ -0,0 +1,153 @@
+// 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.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 long 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,
+ long 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,
+ long serialNumber) {
+ try {
+ PKCS10CertificationRequest bcCsr = csr.getBcCsr();
+ PublicKey publicKey = new JcaPKCS10CertificationRequest(bcCsr).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,
+ long serialNumber) {
+ return new X509CertificateBuilder(subject,
+ subject,
+ notBefore,
+ notAfter,
+ keyPair.getPublic(),
+ keyPair.getPrivate(),
+ signingAlgorithm,
+ serialNumber);
+ }
+
+ 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 X509Certificate build() {
+ try {
+ JcaX509v3CertificateBuilder jcaCertBuilder = new JcaX509v3CertificateBuilder(
+ issuer, BigInteger.valueOf(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/vespajlib/src/main/java/com/yahoo/security/X509CertificateUtils.java b/vespajlib/src/main/java/com/yahoo/security/X509CertificateUtils.java
new file mode 100644
index 00000000000..33bd750bac5
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/main/java/com/yahoo/security/package-info.java b/vespajlib/src/main/java/com/yahoo/security/package-info.java
new file mode 100644
index 00000000000..10a4c9c0e0d
--- /dev/null
+++ b/vespajlib/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/vespajlib/src/test/java/com/yahoo/security/KeyStoreBuilderTest.java b/vespajlib/src/test/java/com/yahoo/security/KeyStoreBuilderTest.java
new file mode 100644
index 00000000000..500c7cb23e1
--- /dev/null
+++ b/vespajlib/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.RSA, 4096);
+ 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/vespajlib/src/test/java/com/yahoo/security/KeyUtilsTest.java b/vespajlib/src/test/java/com/yahoo/security/KeyUtilsTest.java
new file mode 100644
index 00000000000..f414bf8ba84
--- /dev/null
+++ b/vespajlib/src/test/java/com/yahoo/security/KeyUtilsTest.java
@@ -0,0 +1,37 @@
+// 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_private() {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA);
+ PublicKey publicKey = KeyUtils.extractPublicKey(keyPair.getPrivate());
+ assertNotNull(publicKey);
+ }
+
+ @Test
+ public void can_serialize_deserialize_pem() {
+ 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);
+ }
+
+} \ No newline at end of file
diff --git a/vespajlib/src/test/java/com/yahoo/security/Pkcs10CsrBuilderTest.java b/vespajlib/src/test/java/com/yahoo/security/Pkcs10CsrBuilderTest.java
new file mode 100644
index 00000000000..812e26360a4
--- /dev/null
+++ b/vespajlib/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.RSA, 2048);
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA256_WITH_RSA)
+ .addSubjectAlternativeName("san1.com")
+ .addSubjectAlternativeName("san2.com")
+ .build();
+ assertEquals(subject, csr.getSubject());
+ }
+
+} \ No newline at end of file
diff --git a/vespajlib/src/test/java/com/yahoo/security/Pkcs10CsrTest.java b/vespajlib/src/test/java/com/yahoo/security/Pkcs10CsrTest.java
new file mode 100644
index 00000000000..b3141eeca5a
--- /dev/null
+++ b/vespajlib/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.RSA, 2048);
+ SubjectAlternativeName san1 = new SubjectAlternativeName(DNS_NAME, "san1.com");
+ SubjectAlternativeName san2 = new SubjectAlternativeName(DNS_NAME, "san2.com");
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA256_WITH_RSA)
+ .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.RSA, 2048);
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA256_WITH_RSA)
+ .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.RSA, 2048);
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA256_WITH_RSA)
+ .addSubjectAlternativeName("san")
+ .setBasicConstraints(true, true)
+ .build();
+ List<String> expected = Arrays.asList(Extension.BASIC_CONSTRAINS.getOId(), Extension.SUBJECT_ALTERNATIVE_NAMES.getOId());
+ List<String> actual = csr.getExtensionOIds();
+ assertEquals(expected, actual);
+ }
+
+} \ No newline at end of file
diff --git a/vespajlib/src/test/java/com/yahoo/security/Pkcs10CsrUtilsTest.java b/vespajlib/src/test/java/com/yahoo/security/Pkcs10CsrUtilsTest.java
new file mode 100644
index 00000000000..a0457b26f8b
--- /dev/null
+++ b/vespajlib/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.RSA, 2048);
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA256_WITH_RSA).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/vespajlib/src/test/java/com/yahoo/security/SslContextBuilderTest.java b/vespajlib/src/test/java/com/yahoo/security/SslContextBuilderTest.java
new file mode 100644
index 00000000000..6b17520ffe0
--- /dev/null
+++ b/vespajlib/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.RSA, 2048);
+ 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/vespajlib/src/test/java/com/yahoo/security/TestUtils.java b/vespajlib/src/test/java/com/yahoo/security/TestUtils.java
new file mode 100644
index 00000000000..17c9cb99f0e
--- /dev/null
+++ b/vespajlib/src/test/java/com/yahoo/security/TestUtils.java
@@ -0,0 +1,41 @@
+// 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.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.RSA, 4096);
+ 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.SHA256_WITH_RSA, 1)
+ .build();
+ }
+
+ static void createKeystoreFile(Path file, KeyStoreType type, char[] password) {
+ writeKeyStoreToFile(createKeystore(type, password), file, password);
+ }
+}
diff --git a/vespajlib/src/test/java/com/yahoo/security/X509CertificateBuilderTest.java b/vespajlib/src/test/java/com/yahoo/security/X509CertificateBuilderTest.java
new file mode 100644
index 00000000000..36ef9d759a8
--- /dev/null
+++ b/vespajlib/src/test/java/com/yahoo/security/X509CertificateBuilderTest.java
@@ -0,0 +1,59 @@
+// 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.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bjorncs
+ */
+public class X509CertificateBuilderTest {
+
+ @Test
+ public void can_build_self_signed_certificate() throws NoSuchAlgorithmException {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048);
+ X500Principal subject = new X500Principal("CN=myservice");
+ X509Certificate cert =
+ X509CertificateBuilder.fromKeypair(
+ keyPair,
+ subject,
+ Instant.now(),
+ Instant.now().plus(1, ChronoUnit.DAYS),
+ SignatureAlgorithm.SHA256_WITH_RSA,
+ 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.RSA, 2048);
+ Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, csrKeypair, SignatureAlgorithm.SHA256_WITH_RSA).build();
+ KeyPair caKeypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048);
+ X509Certificate cert = X509CertificateBuilder
+ .fromCsr(
+ csr,
+ issuer,
+ Instant.now(),
+ Instant.now().plus(1, ChronoUnit.DAYS),
+ caKeypair.getPrivate(),
+ SignatureAlgorithm.SHA256_WITH_RSA,
+ 1)
+ .addSubjectAlternativeName("subject1.alt")
+ .addSubjectAlternativeName("subject2.alt")
+ .build();
+ assertEquals(subject, cert.getSubjectX500Principal());
+ }
+
+} \ No newline at end of file
diff --git a/vespajlib/src/test/java/com/yahoo/security/X509CertificateUtilsTest.java b/vespajlib/src/test/java/com/yahoo/security/X509CertificateUtilsTest.java
new file mode 100644
index 00000000000..e5605e93e55
--- /dev/null
+++ b/vespajlib/src/test/java/com/yahoo/security/X509CertificateUtilsTest.java
@@ -0,0 +1,73 @@
+// 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.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.RSA, 2048);
+ 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.RSA, 2048);
+ X500Principal subject1 = new X500Principal("CN=myservice");
+ X509Certificate cert1 = TestUtils.createCertificate(keypair, subject1);
+ X500Principal subject2 = new X500Principal("CN=myservice");
+ 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.RSA, 2048);
+ 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.SHA256_WITH_RSA,
+ 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