summaryrefslogtreecommitdiffstats
path: root/athenz-identity-provider-service
diff options
context:
space:
mode:
authorValerij Fredriksen <valerijf@oath.com>2017-11-09 15:44:38 +0100
committerValerij Fredriksen <valerijf@oath.com>2017-11-09 15:44:38 +0100
commitb008228e620c180b2f78542c9104a4eb6d0fd48c (patch)
tree40dc33ccb4ee5ea66ae24e053d8cb482186d982a /athenz-identity-provider-service
parentf8de7b713677b7418b23a9731dc07919fdc4c7bc (diff)
Create certificate request signer
Diffstat (limited to 'athenz-identity-provider-service')
-rw-r--r--athenz-identity-provider-service/pom.xml6
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java124
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerTest.java134
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);
+ }
+ }
+}