aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorValerij Fredriksen <freva@users.noreply.github.com>2017-11-13 15:10:41 +0100
committerGitHub <noreply@github.com>2017-11-13 15:10:41 +0100
commit55c3bc5d24ee6d2ba7c6d886c10629ae7600199a (patch)
tree35062d5882d0757f433c1535581e7ba1609f6d6f
parent01a534d7df2a025678d7c27487f6a5a4f6aa5e46 (diff)
parentd20d62b5622b373f29e00ce1e582ba5c15cccc55 (diff)
Merge pull request #4070 from vespa-engine/freva/certificate-signer
Freva/certificate signer
-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/AthenzInstanceProviderService.java13
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java141
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerServlet.java50
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/model/CertificateSerializedPayload.java68
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/model/CsrSerializedPayload.java62
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java6
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerTest.java134
8 files changed, 478 insertions, 2 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/AthenzInstanceProviderService.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java
index 26a88896fb9..8ac26938633 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java
@@ -8,6 +8,9 @@ import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.Zone;
import com.yahoo.jdisc.http.SecretStore;
import com.yahoo.log.LogLevel;
+import com.yahoo.net.HostName;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca.CertificateSigner;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca.CertificateSignerServlet;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.AthenzCertificateClient;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.CertificateClient;
@@ -64,6 +67,7 @@ public class AthenzInstanceProviderService extends AbstractComponent {
CertificateClient certificateClient,
SslContextFactory sslContextFactory) {
this(config, scheduler, zone, sslContextFactory,
+ new CertificateSigner(keyProvider, getZoneConfig(config, zone), HostName.getLocalhost()),
new InstanceValidator(keyProvider, superModelProvider),
new IdentityDocumentGenerator(config, getZoneConfig(config, zone), nodeRepository, zone, keyProvider),
new AthenzCertificateUpdater(
@@ -74,13 +78,15 @@ public class AthenzInstanceProviderService extends AbstractComponent {
ScheduledExecutorService scheduler,
Zone zone,
SslContextFactory sslContextFactory,
+ CertificateSigner certificateSigner,
InstanceValidator instanceValidator,
IdentityDocumentGenerator identityDocumentGenerator,
AthenzCertificateUpdater reloader) {
// TODO: Enable for all systems. Currently enabled for CD system only
if (SystemName.cd.equals(zone.system())) {
this.scheduler = scheduler;
- this.jetty = createJettyServer(config, sslContextFactory, instanceValidator, identityDocumentGenerator);
+ this.jetty = createJettyServer(config, sslContextFactory,
+ certificateSigner, instanceValidator, identityDocumentGenerator);
// TODO Configurable update frequency
scheduler.scheduleAtFixedRate(reloader, 0, 1, TimeUnit.DAYS);
@@ -97,6 +103,7 @@ public class AthenzInstanceProviderService extends AbstractComponent {
private static Server createJettyServer(AthenzProviderServiceConfig config,
SslContextFactory sslContextFactory,
+ CertificateSigner certificateSigner,
InstanceValidator instanceValidator,
IdentityDocumentGenerator identityDocumentGenerator) {
Server server = new Server();
@@ -105,6 +112,10 @@ public class AthenzInstanceProviderService extends AbstractComponent {
server.addConnector(connector);
ServletHandler handler = new ServletHandler();
+
+ CertificateSignerServlet certificateSignerServlet = new CertificateSignerServlet(certificateSigner);
+ handler.addServletWithMapping(new ServletHolder(certificateSignerServlet), config.apiPath() + "/sign");
+
InstanceConfirmationServlet instanceConfirmationServlet = new InstanceConfirmationServlet(instanceValidator);
handler.addServletWithMapping(new ServletHolder(instanceConfirmationServlet), config.apiPath() + "/instance");
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..2e00695f2f0
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java
@@ -0,0 +1,141 @@
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca;
+
+import com.google.common.collect.ImmutableList;
+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.Provider;
+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;
+
+
+/**
+ * Signs Certificate Signing Reqest from tenant nodes. This certificate will be used
+ * by nodes to authenticate themselves when performing operations against the config
+ * server, such as updating node-repository or orchestrator.
+ *
+ * @author freva
+ */
+public class CertificateSigner {
+
+ private static final Logger log = Logger.getLogger(CertificateSigner.class.getName());
+
+ static final String SIGNER_ALGORITHM = "SHA256withRSA";
+ static final Duration CERTIFICATE_EXPIRATION = Duration.ofDays(30);
+ private static final List<ASN1ObjectIdentifier> ILLEGAL_EXTENSIONS = ImmutableList.of(
+ Extension.basicConstraints, Extension.subjectAlternativeName);
+
+ private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
+ private final Provider provider = new BouncyCastleProvider();
+
+ 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;
+ }
+
+ /**
+ * Signs the CSR if:
+ * <ul>
+ * <li>Common Name matches {@code remoteHostname}</li>
+ * <li>CSR does not contain any any of the extensions in {@code ILLEGAL_EXTENSIONS}</li>
+ * </ul>
+ */
+ X509Certificate generateX509Certificate(PKCS10CertificationRequest certReq, String remoteHostname) {
+ verifyCertificateCommonName(certReq.getSubject(), remoteHostname);
+ verifyCertificateExtensions(certReq);
+
+ Date notBefore = Date.from(clock.instant());
+ Date notAfter = Date.from(clock.instant().plus(CERTIFICATE_EXPIRATION));
+
+ 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, true, new BasicConstraints(false));
+
+ ContentSigner caSigner = new JcaContentSignerBuilder(SIGNER_ALGORITHM).build(caPrivateKey);
+
+ return certificateConverter
+ .setProvider(provider)
+ .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 verifyCertificateCommonName(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);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ static void verifyCertificateExtensions(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/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerServlet.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerServlet.java
new file mode 100644
index 00000000000..d2ebae394a2
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerServlet.java
@@ -0,0 +1,50 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca.model.CertificateSerializedPayload;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca.model.CsrSerializedPayload;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils;
+import org.bouncycastle.pkcs.PKCS10CertificationRequest;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+import java.util.logging.Logger;
+
+/**
+ * @author freva
+ */
+public class CertificateSignerServlet extends HttpServlet {
+
+ private static final Logger log = Logger.getLogger(CertificateSignerServlet.class.getName());
+
+ private final CertificateSigner certificateSigner;
+
+ public CertificateSignerServlet(CertificateSigner certificateSigner) {
+ this.certificateSigner = certificateSigner;
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ try {
+ String remoteHostname = req.getRemoteHost();
+ PKCS10CertificationRequest csr = Utils.getMapper().readValue(req.getReader(), CsrSerializedPayload.class).csr;
+
+ log.log(LogLevel.DEBUG, "Certification request from " + remoteHostname + ": " + csr);
+
+ X509Certificate certificate = certificateSigner.generateX509Certificate(csr, remoteHostname);
+ CertificateSerializedPayload certificateSerializedPayload = new CertificateSerializedPayload(certificate);
+
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentType("application/json");
+ resp.getWriter().write(Utils.getMapper().writeValueAsString(certificateSerializedPayload));
+ } catch (RuntimeException e) {
+ log.log(LogLevel.ERROR, e.getMessage(), e);
+ resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/model/CertificateSerializedPayload.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/model/CertificateSerializedPayload.java
new file mode 100644
index 00000000000..2fd34741da7
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/model/CertificateSerializedPayload.java
@@ -0,0 +1,68 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca.model;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.util.io.pem.PemObject;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+
+/**
+ * Contains PEM formatted signed certificate
+ *
+ * @author freva
+ */
+public class CertificateSerializedPayload {
+
+ @JsonProperty("certificate") @JsonSerialize(using = CertificateSerializer.class)
+ public final X509Certificate certificate;
+
+ @JsonCreator
+ public CertificateSerializedPayload(@JsonProperty("certificate") X509Certificate certificate) {
+ this.certificate = certificate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ CertificateSerializedPayload that = (CertificateSerializedPayload) o;
+
+ return certificate.equals(that.certificate);
+ }
+
+ @Override
+ public int hashCode() {
+ return certificate.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "CertificateSerializedPayload{" +
+ "certificate='" + certificate + '\'' +
+ '}';
+ }
+
+ public static class CertificateSerializer extends JsonSerializer<X509Certificate> {
+ @Override
+ public void serialize(
+ X509Certificate certificate, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
+ pemWriter.writeObject(new PemObject("CERTIFICATE", certificate.getEncoded()));
+ pemWriter.flush();
+ gen.writeString(stringWriter.toString());
+ } catch (CertificateEncodingException e) {
+ throw new RuntimeException("Failed to encode X509Certificate", e);
+ }
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/model/CsrSerializedPayload.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/model/CsrSerializedPayload.java
new file mode 100644
index 00000000000..d755fbd02a3
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/model/CsrSerializedPayload.java
@@ -0,0 +1,62 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca.model;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.pkcs.PKCS10CertificationRequest;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * Contains PEM formatted Certificate Signing Request (CSR)
+ *
+ * @author freva
+ */
+public class CsrSerializedPayload {
+
+ @JsonProperty("csr") public final PKCS10CertificationRequest csr;
+
+ @JsonCreator
+ public CsrSerializedPayload(@JsonProperty("csr") @JsonDeserialize(using = CertificateRequestDeserializer.class)
+ PKCS10CertificationRequest csr) {
+ this.csr = csr;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ CsrSerializedPayload that = (CsrSerializedPayload) o;
+
+ return csr.equals(that.csr);
+ }
+
+ @Override
+ public int hashCode() {
+ return csr.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "CsrSerializedPayload{" +
+ "csr='" + csr + '\'' +
+ '}';
+ }
+
+ public static class CertificateRequestDeserializer extends JsonDeserializer<PKCS10CertificationRequest> {
+ @Override
+ public PKCS10CertificationRequest deserialize(
+ JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
+ try (PEMParser pemParser = new PEMParser(new StringReader(jsonParser.getValueAsString()))) {
+ return (PKCS10CertificationRequest) pemParser.readObject();
+ }
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java
index bf0746aee7e..c8c3826fc39 100644
--- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java
@@ -9,6 +9,7 @@ import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.Zone;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.AthenzInstanceProviderService.AthenzCertificateUpdater;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca.CertificateSigner;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.CertificateClient;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.IdentityDocumentGenerator;
@@ -101,13 +102,16 @@ public class AthenzInstanceProviderServiceTest {
ScheduledExecutorService executor = mock(ScheduledExecutorService.class);
when(executor.awaitTermination(anyLong(), any())).thenReturn(true);
+ CertificateSigner certificateSigner = mock(CertificateSigner.class);
+
InstanceValidator instanceValidator = mock(InstanceValidator.class);
when(instanceValidator.isValidInstance(any())).thenReturn(true);
IdentityDocumentGenerator identityDocumentGenerator = mock(IdentityDocumentGenerator.class);
AthenzInstanceProviderService athenzInstanceProviderService = new AthenzInstanceProviderService(
- config, executor, ZONE, sslContextFactory, instanceValidator, identityDocumentGenerator, certificateUpdater);
+ config, executor, ZONE, sslContextFactory, certificateSigner, instanceValidator,
+ identityDocumentGenerator, certificateUpdater);
try (CloseableHttpClient client = createHttpClient(domain, service)) {
assertFalse(getStatus(client));
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..e691da0b2c3
--- /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.verifyCertificateCommonName(
+ new X500Name("CN=" + requestersHostname), requestersHostname);
+ CertificateSigner.verifyCertificateCommonName(
+ new X500Name("C=NO,OU=Vespa,CN=" + requestersHostname), requestersHostname);
+ CertificateSigner.verifyCertificateCommonName(
+ 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.verifyCertificateExtensions(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.verifyCertificateExtensions(request);
+ }
+
+ private void assertCertificateCommonNameException(String subject, String expectedMessage) {
+ try {
+ CertificateSigner.verifyCertificateCommonName(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(startTime, certificate.getNotBefore().getTime());
+ assertEquals(startTime + CertificateSigner.CERTIFICATE_EXPIRATION.toMillis(), certificate.getNotAfter().getTime());
+ 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);
+ }
+ }
+}