diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2018-08-30 16:35:55 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2018-08-30 17:53:09 +0200 |
commit | 410fd34a61b062a2e8fb57752dbba16cb1d1471f (patch) | |
tree | 087aff106d3c928c3c69ebf876b354eed783b8a1 /vespajlib | |
parent | c3ad87b12928fb61e456cbf3e0c5b61221750816 (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')
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 |