diff options
author | Valerij Fredriksen <valerijf@oath.com> | 2017-11-09 15:44:38 +0100 |
---|---|---|
committer | Valerij Fredriksen <valerijf@oath.com> | 2017-11-09 15:44:38 +0100 |
commit | b008228e620c180b2f78542c9104a4eb6d0fd48c (patch) | |
tree | 40dc33ccb4ee5ea66ae24e053d8cb482186d982a /athenz-identity-provider-service | |
parent | f8de7b713677b7418b23a9731dc07919fdc4c7bc (diff) |
Create certificate request signer
Diffstat (limited to 'athenz-identity-provider-service')
3 files changed, 264 insertions, 0 deletions
diff --git a/athenz-identity-provider-service/pom.xml b/athenz-identity-provider-service/pom.xml index 26e24be526c..260836af892 100644 --- a/athenz-identity-provider-service/pom.xml +++ b/athenz-identity-provider-service/pom.xml @@ -102,6 +102,12 @@ <scope>test</scope> </dependency> <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>testutil</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.4.1</version> diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java new file mode 100644 index 00000000000..2b91735b104 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java @@ -0,0 +1,124 @@ +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca; + +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.KeyProvider; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERUTF8String; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.AttributeTypeAndValue; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; + +import java.math.BigInteger; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.time.Clock; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +/** + * @author freva + */ +public class CertificateSigner { + + private static final Logger log = Logger.getLogger(CertificateSigner.class.getName()); + + static final String SIGNER_ALGORITHM = "SHA256withRSA"; + private static final Duration CERTIIFICATE_DURATION = Duration.ofDays(30); + private static final List<ASN1ObjectIdentifier> ILLEGAL_EXTENSIONS = Arrays.asList( + Extension.basicConstraints, Extension.subjectAlternativeName); + + private final PrivateKey caPrivateKey; + private final X500Name issuer; + private final Clock clock; + + public CertificateSigner(KeyProvider keyProvider, + AthenzProviderServiceConfig.Zones zoneConfig, + String configServerHostname) { + this(keyProvider.getPrivateKey(zoneConfig.secretVersion()), configServerHostname, Clock.systemUTC()); + } + + CertificateSigner(PrivateKey caPrivateKey, String configServerHostname, Clock clock) { + this.caPrivateKey = caPrivateKey; + this.issuer = new X500Name("CN=" + configServerHostname); + this.clock = clock; + } + + X509Certificate generateX509Certificate(PKCS10CertificationRequest certReq, String remoteHostname) { + assertCertificateCommonName(certReq.getSubject(), remoteHostname); + assertCertificateExtensions(certReq); + + Date notBefore = Date.from(clock.instant()); + Date notAfter = Date.from(clock.instant().plus(CERTIIFICATE_DURATION)); + + try { + PublicKey publicKey = new JcaPKCS10CertificationRequest(certReq).getPublicKey(); + X509v3CertificateBuilder caBuilder = new JcaX509v3CertificateBuilder( + issuer, BigInteger.valueOf(clock.millis()), notBefore, notAfter, certReq.getSubject(), publicKey) + + // Set Basic Constraints to false + .addExtension(Extension.basicConstraints, false, new BasicConstraints(false)); + + ContentSigner caSigner = new JcaContentSignerBuilder(SIGNER_ALGORITHM).build(caPrivateKey); + + return new JcaX509CertificateConverter() + .setProvider(new BouncyCastleProvider()) + .getCertificate(caBuilder.build(caSigner)); + } catch (Exception ex) { + log.log(LogLevel.ERROR, "Failed to generate X509 Certificate", ex); + throw new RuntimeException("Failed to generate X509 Certificate"); + } + } + + static void assertCertificateCommonName(X500Name subject, String commonName) { + List<AttributeTypeAndValue> attributesAndValues = Arrays.stream(subject.getRDNs()) + .flatMap(rdn -> rdn.isMultiValued() ? + Stream.of(rdn.getTypesAndValues()) : Stream.of(rdn.getFirst())) + .filter(attr -> attr.getType() == BCStyle.CN) + .collect(Collectors.toList()); + + if (attributesAndValues.size() != 1) { + throw new IllegalArgumentException("Only 1 common name should be set"); + } + + String actualCommonName = DERUTF8String.getInstance(attributesAndValues.get(0).getValue()).getString(); + if (! actualCommonName.equals(commonName)) { + throw new IllegalArgumentException("Expected common name to be " + commonName + ", but was " + actualCommonName); + } + } + + static void assertCertificateExtensions(PKCS10CertificationRequest request) { + List<String> illegalExt = Arrays + .stream(request.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest)) + .map(attribute -> Extensions.getInstance(attribute.getAttrValues().getObjectAt(0))) + .flatMap(ext -> Collections.list((Enumeration<ASN1ObjectIdentifier>) ext.oids()).stream()) + .filter(ILLEGAL_EXTENSIONS::contains) + .map(ASN1ObjectIdentifier::getId) + .collect(Collectors.toList()); + + if (! illegalExt.isEmpty()) { + throw new IllegalArgumentException("CSR contains illegal extensions: " + String.join(", ", illegalExt)); + } + } +} diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerTest.java new file mode 100644 index 00000000000..70ddbd74ff3 --- /dev/null +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerTest.java @@ -0,0 +1,134 @@ +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca; + +import com.yahoo.test.ManualClock; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +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.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; +import org.junit.Test; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author freva + */ +public class CertificateSignerTest { + + private final KeyPair clientKeyPair = getKeyPair(); + + private final long startTime = 1234567890000L; + private final KeyPair caKeyPair = getKeyPair(); + private final String cfgServerHostname = "cfg1.us-north-1.vespa.domain.tld"; + private final ManualClock clock = new ManualClock(Instant.ofEpochMilli(startTime)); + private final CertificateSigner signer = new CertificateSigner(caKeyPair.getPrivate(), cfgServerHostname, clock); + + private final String requestersHostname = "tenant-123.us-north-1.vespa.domain.tld"; + + @Test + public void test_signing() throws Exception { + ExtensionsGenerator extGen = new ExtensionsGenerator(); + String subject = "C=NO,OU=Vespa,CN=" + requestersHostname; + PKCS10CertificationRequest request = makeRequest(subject, extGen.generate()); + + X509Certificate certificate = signer.generateX509Certificate(request, requestersHostname); + assertCertificate(certificate, subject, Collections.singleton(Extension.basicConstraints.getId())); + } + + @Test + public void common_name_test() throws Exception { + CertificateSigner.assertCertificateCommonName( + new X500Name("CN=" + requestersHostname), requestersHostname); + CertificateSigner.assertCertificateCommonName( + new X500Name("C=NO,OU=Vespa,CN=" + requestersHostname), requestersHostname); + CertificateSigner.assertCertificateCommonName( + new X500Name("C=NO+OU=org,CN=" + requestersHostname), requestersHostname); + + assertCertificateCommonNameException("C=NO", "Only 1 common name should be set"); + assertCertificateCommonNameException("C=US+CN=abc123.domain.tld,C=NO+CN=" + requestersHostname, "Only 1 common name should be set"); + assertCertificateCommonNameException("CN=evil.hostname.domain.tld", + "Expected common name to be tenant-123.us-north-1.vespa.domain.tld, but was evil.hostname.domain.tld"); + } + + @Test(expected = IllegalArgumentException.class) + public void extensions_test_subject_alternative_names() throws Exception { + ExtensionsGenerator extGen = new ExtensionsGenerator(); + extGen.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(new GeneralName[] { + new GeneralName(GeneralName.dNSName, "some.other.domain.tld")})); + PKCS10CertificationRequest request = makeRequest("OU=Vespa", extGen.generate()); + + CertificateSigner.assertCertificateExtensions(request); + } + + @Test + public void extensions_allowed() throws Exception { + ExtensionsGenerator extGen = new ExtensionsGenerator(); + extGen.addExtension(Extension.certificateIssuer, true, new byte[0]); + PKCS10CertificationRequest request = makeRequest("OU=Vespa", extGen.generate()); + + CertificateSigner.assertCertificateExtensions(request); + } + + private void assertCertificateCommonNameException(String subject, String expectedMessage) { + try { + CertificateSigner.assertCertificateCommonName(new X500Name(subject), requestersHostname); + fail("Expected to fail"); + } catch (IllegalArgumentException e) { + assertEquals(expectedMessage, e.getMessage()); + } + } + + private void assertCertificate(X509Certificate certificate, String expectedSubjectName, Set<String> expectedExtensions) throws Exception { + assertEquals(3, certificate.getVersion()); + assertEquals(BigInteger.valueOf(startTime), certificate.getSerialNumber()); + assertEquals("Sat Feb 14 00:31:30 CET 2009", certificate.getNotBefore().toString()); + assertEquals("Mon Mar 16 00:31:30 CET 2009", certificate.getNotAfter().toString()); + assertEquals(CertificateSigner.SIGNER_ALGORITHM, certificate.getSigAlgName()); + assertEquals(expectedSubjectName, certificate.getSubjectDN().getName()); + assertEquals("CN=" + cfgServerHostname, certificate.getIssuerX500Principal().getName()); + + Set<String> extensions = Stream.of(certificate.getNonCriticalExtensionOIDs(), + certificate.getCriticalExtensionOIDs()) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + assertEquals(expectedExtensions, extensions); + + certificate.verify(caKeyPair.getPublic()); + } + + private PKCS10CertificationRequest makeRequest(String subject, Extensions extensions) throws Exception { + PKCS10CertificationRequestBuilder builder = new JcaPKCS10CertificationRequestBuilder( + new X500Name(subject), clientKeyPair.getPublic()); + builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensions); + + ContentSigner signGen = new JcaContentSignerBuilder(CertificateSigner.SIGNER_ALGORITHM).build(caKeyPair.getPrivate()); + return builder.build(signGen); + } + + private static KeyPair getKeyPair() { + try { + return KeyPairGenerator.getInstance("RSA").genKeyPair(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} |