diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2018-04-17 16:12:35 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2018-04-17 16:25:32 +0200 |
commit | b65667463f7add415a7b1240da7a22ae384b1942 (patch) | |
tree | c21744a18496b9f07197bcc8624a419243b2251e /vespa-athenz | |
parent | b9f6244b3cf0830ad423b41732e0279285bce7b8 (diff) |
Add helper for extracting SANs from certificate
- Model SAN as type SubjectAlternativeName
- Add SubjectAlternativeName to csr and certificate builders
Diffstat (limited to 'vespa-athenz')
7 files changed, 188 insertions, 29 deletions
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10Csr.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10Csr.java index d9cd3141f19..e0029681b23 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10Csr.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10Csr.java @@ -1,15 +1,11 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.athenz.tls; -import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERIA5String; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; -import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.BasicConstraints; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.Extensions; -import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.pkcs.PKCS10CertificationRequest; @@ -40,12 +36,10 @@ public class Pkcs10Csr { return new X500Principal(csr.getSubject().toString()); } - public List<String> getSubjectAlternativeNames() { + public List<SubjectAlternativeName> getSubjectAlternativeNames() { return getExtensions() .map(extensions -> GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName)) - .map(generalNames -> Arrays.stream(generalNames.getNames()) - .map(Pkcs10Csr::toString) - .collect(toList())) + .map(SubjectAlternativeName::fromGeneralNames) .orElse(emptyList()); } @@ -74,17 +68,4 @@ public class Pkcs10Csr { .map(attribute -> Extensions.getInstance(attribute.getAttrValues().getObjectAt(0))); } - private static String toString(GeneralName generalName) { - ASN1Encodable name = generalName.getName(); - switch (generalName.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(); - } - } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrBuilder.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrBuilder.java index 9c2cd9d4d9f..29be201fb43 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrBuilder.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrBuilder.java @@ -20,6 +20,8 @@ import java.security.KeyPair; import java.util.ArrayList; import java.util.List; +import static com.yahoo.vespa.athenz.tls.SubjectAlternativeName.Type.DNS_NAME; + /** * @author bjorncs */ @@ -27,7 +29,7 @@ public class Pkcs10CsrBuilder { private final X500Principal subject; private final KeyPair keyPair; - private final List<String> subjectAlternativeNames = new ArrayList<>(); + private final List<SubjectAlternativeName> subjectAlternativeNames = new ArrayList<>(); private final SignatureAlgorithm signatureAlgorithm; private BasicConstraintsExtension basicConstraintsExtension; @@ -45,7 +47,12 @@ public class Pkcs10CsrBuilder { return new Pkcs10CsrBuilder(subject, keyPair, signatureAlgorithm); } - public Pkcs10CsrBuilder addSubjectAlternativeName(String san) { + 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; } @@ -69,7 +76,7 @@ public class Pkcs10CsrBuilder { if (!subjectAlternativeNames.isEmpty()) { GeneralNames generalNames = new GeneralNames( subjectAlternativeNames.stream() - .map(san -> new GeneralName(GeneralName.dNSName, san)) + .map(SubjectAlternativeName::toGeneralName) .toArray(GeneralName[]::new)); extGen.addExtension(Extension.subjectAlternativeName, false, generalNames); } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/SubjectAlternativeName.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/SubjectAlternativeName.java new file mode 100644 index 00000000000..8b89fc6fe7f --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/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.vespa.athenz.tls; + +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/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateBuilder.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateBuilder.java index ba22af13fd2..c27b704f6a3 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateBuilder.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateBuilder.java @@ -27,6 +27,8 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import static com.yahoo.vespa.athenz.tls.SubjectAlternativeName.Type.DNS_NAME; + /** * @author bjorncs */ @@ -37,7 +39,7 @@ public class X509CertificateBuilder { private final PrivateKey caPrivateKey; private final Instant notBefore; private final Instant notAfter; - private final List<String> subjectAlternativeNames = new ArrayList<>(); + private final List<SubjectAlternativeName> subjectAlternativeNames = new ArrayList<>(); private final X500Principal issuer; private final X500Principal subject; private final PublicKey certPublicKey; @@ -102,7 +104,12 @@ public class X509CertificateBuilder { serialNumber); } - public X509CertificateBuilder addSubjectAlternativeName(String san) { + 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; } @@ -125,7 +132,7 @@ public class X509CertificateBuilder { if (!subjectAlternativeNames.isEmpty()) { GeneralNames generalNames = new GeneralNames( subjectAlternativeNames.stream() - .map(san -> new GeneralName(GeneralName.dNSName, san)) + .map(SubjectAlternativeName::toGeneralName) .toArray(GeneralName[]::new)); jcaCertBuilder.addExtension(Extension.subjectAlternativeName, false, generalNames); } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java index 3e1bd3eb31c..707666ae3c8 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java @@ -1,6 +1,10 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.athenz.tls; +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; @@ -16,9 +20,14 @@ import java.io.StringWriter; import java.io.UncheckedIOException; import java.security.GeneralSecurityException; import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import static com.yahoo.vespa.athenz.tls.Extension.SUBJECT_ALTERNATIVE_NAMES; import static java.util.stream.Collectors.toList; /** @@ -75,4 +84,19 @@ public class X509CertificateUtils { } } + + 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/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrTest.java index bb2e80ba705..ea60511f39c 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrTest.java @@ -7,6 +7,7 @@ import java.security.KeyPair; import java.util.Arrays; import java.util.List; +import static com.yahoo.vespa.athenz.tls.SubjectAlternativeName.Type.DNS_NAME; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -19,8 +20,8 @@ public class Pkcs10CsrTest { public void can_read_subject_alternative_names() { X500Principal subject = new X500Principal("CN=subject"); KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); - String san1 = "san1.com"; - String san2 = "san2.com"; + 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) diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java index 847f49bf537..cc203011c0b 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java @@ -1,5 +1,6 @@ package com.yahoo.vespa.athenz.tls; +import org.hamcrest.Matchers; import org.junit.Test; import javax.security.auth.x500.X500Principal; @@ -7,8 +8,12 @@ import java.security.KeyPair; import java.security.cert.X509Certificate; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.List; +import static com.yahoo.vespa.athenz.tls.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; @@ -38,4 +43,24 @@ public class X509CertificateUtilsTest { } + @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 |