diff options
Diffstat (limited to 'athenz-identity-provider-service/src')
31 files changed, 844 insertions, 736 deletions
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 deleted file mode 100644 index 26a88896fb9..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java +++ /dev/null @@ -1,207 +0,0 @@ -// 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; - -import com.google.inject.Inject; -import com.yahoo.component.AbstractComponent; -import com.yahoo.config.model.api.SuperModelProvider; -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.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.AthenzCertificateClient; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.CertificateClient; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.IdentityDocumentGenerator; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.IdentityDocumentServlet; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.InstanceConfirmationServlet; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.InstanceValidator; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.KeyProvider; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.SecretStoreKeyProvider; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.StatusServlet; -import com.yahoo.vespa.hosted.provision.NodeRepository; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.time.Duration; -import java.time.temporal.TemporalAmount; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; - -/** - * A component acting as both SIA for configserver and provides a lightweight Jetty instance hosting the InstanceConfirmation API - * - * @author bjorncs - */ -public class AthenzInstanceProviderService extends AbstractComponent { - - private static final Logger log = Logger.getLogger(AthenzInstanceProviderService.class.getName()); - - private final ScheduledExecutorService scheduler; - private final Server jetty; - - @Inject - public AthenzInstanceProviderService(AthenzProviderServiceConfig config, SuperModelProvider superModelProvider, - NodeRepository nodeRepository, Zone zone, SecretStore secretStore) { - this(config, new SecretStoreKeyProvider(secretStore, getZoneConfig(config, zone).secretName()), Executors.newSingleThreadScheduledExecutor(), - superModelProvider, nodeRepository, zone, new AthenzCertificateClient(config, getZoneConfig(config, zone)), createSslContextFactory()); - } - - private AthenzInstanceProviderService(AthenzProviderServiceConfig config, - KeyProvider keyProvider, - ScheduledExecutorService scheduler, - SuperModelProvider superModelProvider, - NodeRepository nodeRepository, - Zone zone, - CertificateClient certificateClient, - SslContextFactory sslContextFactory) { - this(config, scheduler, zone, sslContextFactory, - new InstanceValidator(keyProvider, superModelProvider), - new IdentityDocumentGenerator(config, getZoneConfig(config, zone), nodeRepository, zone, keyProvider), - new AthenzCertificateUpdater( - certificateClient, sslContextFactory, keyProvider, config, getZoneConfig(config, zone))); - } - - AthenzInstanceProviderService(AthenzProviderServiceConfig config, - ScheduledExecutorService scheduler, - Zone zone, - SslContextFactory sslContextFactory, - 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); - - // TODO Configurable update frequency - scheduler.scheduleAtFixedRate(reloader, 0, 1, TimeUnit.DAYS); - try { - jetty.start(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } else { - this.scheduler = null; - this.jetty = null; - } - } - - private static Server createJettyServer(AthenzProviderServiceConfig config, - SslContextFactory sslContextFactory, - InstanceValidator instanceValidator, - IdentityDocumentGenerator identityDocumentGenerator) { - Server server = new Server(); - ServerConnector connector = new ServerConnector(server, sslContextFactory); - connector.setPort(config.port()); - server.addConnector(connector); - - ServletHandler handler = new ServletHandler(); - InstanceConfirmationServlet instanceConfirmationServlet = new InstanceConfirmationServlet(instanceValidator); - handler.addServletWithMapping(new ServletHolder(instanceConfirmationServlet), config.apiPath() + "/instance"); - - IdentityDocumentServlet identityDocumentServlet = new IdentityDocumentServlet(identityDocumentGenerator); - handler.addServletWithMapping(new ServletHolder(identityDocumentServlet), config.apiPath() + "/identity-document"); - - handler.addServletWithMapping(StatusServlet.class, "/status.html"); - server.setHandler(handler); - return server; - - } - - private static AthenzProviderServiceConfig.Zones getZoneConfig(AthenzProviderServiceConfig config, Zone zone) { - String key = zone.environment().value() + "." + zone.region().value(); - return config.zones(key); - } - - static SslContextFactory createSslContextFactory() { - try { - SslContextFactory sslContextFactory = new SslContextFactory(); - sslContextFactory.setWantClientAuth(true); - sslContextFactory.setProtocol("TLS"); - sslContextFactory.setKeyManagerFactoryAlgorithm("SunX509"); - return sslContextFactory; - } catch (Exception e) { - throw new IllegalArgumentException("Failed to create SSL context factory: " + e.getMessage(), e); - } - } - - static class AthenzCertificateUpdater implements Runnable { - - // TODO Make expiry a configuration parameter - private static final TemporalAmount EXPIRY_TIME = Duration.ofDays(30); - private static final Logger log = Logger.getLogger(AthenzCertificateUpdater.class.getName()); - - private final CertificateClient certificateClient; - private final SslContextFactory sslContextFactory; - private final KeyProvider keyProvider; - private final AthenzProviderServiceConfig config; - private final AthenzProviderServiceConfig.Zones zoneConfig; - - AthenzCertificateUpdater(CertificateClient certificateClient, - SslContextFactory sslContextFactory, - KeyProvider keyProvider, - AthenzProviderServiceConfig config, - AthenzProviderServiceConfig.Zones zoneConfig) { - this.certificateClient = certificateClient; - this.sslContextFactory = sslContextFactory; - this.keyProvider = keyProvider; - this.config = config; - this.zoneConfig = zoneConfig; - } - - @Override - public void run() { - try { - log.log(LogLevel.INFO, "Updating Athenz certificate through ZTS"); - PrivateKey privateKey = keyProvider.getPrivateKey(zoneConfig.secretVersion()); - X509Certificate certificate = certificateClient.updateCertificate(privateKey, EXPIRY_TIME); - - String dummyPassword = "athenz"; - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(null); - keyStore.setKeyEntry("athenz", - privateKey, - dummyPassword.toCharArray(), - new Certificate[]{certificate}); - - sslContextFactory.reload(sslContextFactory -> { - sslContextFactory.setKeyStore(keyStore); - sslContextFactory.setKeyStorePassword(dummyPassword); - }); - log.log(LogLevel.INFO, "Athenz certificate reload successfully completed"); - } catch (Throwable e) { - log.log(LogLevel.ERROR, "Failed to update certificate from ZTS: " + e.getMessage(), e); - } - } - } - - @Override - public void deconstruct() { - try { - // TODO: Fix deconstruct when setup properly in all zones - log.log(LogLevel.INFO, "Deconstructing Athenz provider service"); - if(scheduler != null) - scheduler.shutdown(); - if(jetty != null) - jetty.stop(); - if (scheduler != null && !scheduler.awaitTermination(1, TimeUnit.MINUTES)) { - log.log(LogLevel.ERROR, "Failed to stop certificate updater"); - } - } catch (InterruptedException e) { - log.log(LogLevel.ERROR, "Failed to stop certificate updater: " + e.getMessage(), e); - } catch (Exception e) { - log.log(LogLevel.ERROR, "Failed to stop Jetty: " + e.getMessage(), e); - } finally { - super.deconstruct(); - } - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java new file mode 100644 index 00000000000..7910650ed5e --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java @@ -0,0 +1,117 @@ +// 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; + +import com.google.inject.Inject; +import com.yahoo.component.AbstractComponent; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Zone; +import com.yahoo.jdisc.http.ssl.SslKeyStoreConfigurator; +import com.yahoo.jdisc.http.ssl.SslKeyStoreContext; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.AthenzCertificateClient; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils.getZoneConfig; + +/** + * @author bjorncs + */ +// TODO Cache certificate on disk +@SuppressWarnings("unused") // Component injected into Jetty connector factory +public class AthenzSslKeyStoreConfigurator extends AbstractComponent implements SslKeyStoreConfigurator { + private static final Logger log = Logger.getLogger(AthenzSslKeyStoreConfigurator.class.getName()); + // TODO Make expiry and update frequency configurable parameters + private static final Duration CERTIFICATE_EXPIRY_TIME = Duration.ofDays(30); + private static final Duration CERTIFICATE_UPDATE_PERIOD = Duration.ofDays(7); + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final AthenzCertificateClient certificateClient; + private final KeyProvider keyProvider; + private final AthenzProviderServiceConfig.Zones zoneConfig; + private final AtomicBoolean alreadyConfigured = new AtomicBoolean(); + private final Zone zone; + + @Inject + public AthenzSslKeyStoreConfigurator(KeyProvider keyProvider, + AthenzProviderServiceConfig config, + Zone zone) { + AthenzProviderServiceConfig.Zones zoneConfig = getZoneConfig(config, zone); + this.certificateClient = new AthenzCertificateClient(config, zoneConfig); + this.keyProvider = keyProvider; + this.zoneConfig = zoneConfig; + this.zone = zone; + } + + @Override + public void configure(SslKeyStoreContext sslKeyStoreContext) { + // TODO Remove this when main is ready + if (zone.system() != SystemName.cd) { + return; + } + if (alreadyConfigured.getAndSet(true)) { // For debugging purpose of SslKeyStoreConfigurator interface + throw new IllegalStateException("Already configured. configure() can only be called once."); + } + AthenzCertificateUpdater updater = new AthenzCertificateUpdater(sslKeyStoreContext); + scheduler.scheduleAtFixedRate(updater, /*initialDelay*/0, CERTIFICATE_UPDATE_PERIOD.toMinutes(), TimeUnit.MINUTES); + } + + @Override + public void deconstruct() { + try { + scheduler.shutdownNow(); + scheduler.awaitTermination(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to shutdown Athenz certificate updater on time", e); + } + } + + private class AthenzCertificateUpdater implements Runnable { + + private final SslKeyStoreContext sslKeyStoreContext; + + AthenzCertificateUpdater(SslKeyStoreContext sslKeyStoreContext) { + this.sslKeyStoreContext = sslKeyStoreContext; + } + + @Override + public void run() { + try { + log.log(LogLevel.INFO, "Updating Athenz certificate from ZTS"); + PrivateKey privateKey = keyProvider.getPrivateKey(zoneConfig.secretVersion()); + X509Certificate certificate = certificateClient.updateCertificate(privateKey, CERTIFICATE_EXPIRY_TIME); + verifyActualExperiy(certificate); + + String dummyPassword = "athenz"; + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null); + keyStore.setKeyEntry("athenz", privateKey, dummyPassword.toCharArray(), new Certificate[]{certificate}); + sslKeyStoreContext.updateKeyStore(keyStore, dummyPassword); + log.log(LogLevel.INFO, "Athenz certificate reload successfully completed"); + } catch (Throwable e) { + log.log(LogLevel.ERROR, "Failed to update certificate from ZTS: " + e.getMessage(), e); + } + } + + private void verifyActualExperiy(X509Certificate certificate) { + Instant notAfter = certificate.getNotAfter().toInstant(); + Instant notBefore = certificate.getNotBefore().toInstant(); + if (!notBefore.plus(CERTIFICATE_EXPIRY_TIME).equals(notAfter)) { + Duration actualExpiry = Duration.between(notBefore, notAfter); + log.log(LogLevel.WARNING, + String.format("Expected expiry %s, got %s", CERTIFICATE_EXPIRY_TIME, actualExpiry)); + } + } + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/KeyProvider.java index 5a1d7e3c1ff..a72a2fcbc6c 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/KeyProvider.java @@ -1,5 +1,5 @@ // 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.impl; +package com.yahoo.vespa.hosted.athenz.instanceproviderservice; import java.security.PrivateKey; import java.security.PublicKey; diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSerializedPayload.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSerializedPayload.java new file mode 100644 index 00000000000..25733bf0075 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/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; + +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/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..2dc3f24664c --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java @@ -0,0 +1,150 @@ +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import com.yahoo.config.provision.Zone; +import com.yahoo.log.LogLevel; +import com.yahoo.net.HostName; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; +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; + +import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils.getZoneConfig; + + +/** + * 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; + + @Inject + public CertificateSigner(KeyProvider keyProvider, AthenzProviderServiceConfig config, Zone zone) { + this(getPrivateKey(keyProvider, config, zone), HostName.getLocalhost(), 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)); + } + } + + private static PrivateKey getPrivateKey(KeyProvider keyProvider, AthenzProviderServiceConfig config, Zone zone) { + AthenzProviderServiceConfig.Zones zoneConfig = getZoneConfig(config, zone); + return keyProvider.getPrivateKey(zoneConfig.secretVersion()); + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerResource.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerResource.java new file mode 100644 index 00000000000..417acf0e9b5 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerResource.java @@ -0,0 +1,52 @@ +// 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.google.inject.Inject; +import com.yahoo.container.jaxrs.annotation.Component; +import com.yahoo.log.LogLevel; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import java.security.cert.X509Certificate; +import java.util.logging.Logger; + +/** + * @author bjorncs + * @author freva + */ +@Path("/sign") +public class CertificateSignerResource { + + private static final Logger log = Logger.getLogger(CertificateSignerResource.class.getName()); + + private final CertificateSigner certificateSigner; + + @Inject + public CertificateSignerResource(@Component CertificateSigner certificateSigner) { + this.certificateSigner = certificateSigner; + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public CertificateSerializedPayload generateCertificate(CsrSerializedPayload csrPayload, + @Context HttpServletRequest req) { + try { + String remoteHostname = req.getRemoteHost(); + PKCS10CertificationRequest csr = csrPayload.csr; + log.log(LogLevel.DEBUG, "Certification request from " + remoteHostname + ": " + csr); + X509Certificate certificate = certificateSigner.generateX509Certificate(csr, remoteHostname); + return new CertificateSerializedPayload(certificate); + } catch (RuntimeException e) { + log.log(LogLevel.ERROR, e.getMessage(), e); + throw new InternalServerErrorException(e.getMessage(), e); + } + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CsrSerializedPayload.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CsrSerializedPayload.java new file mode 100644 index 00000000000..f56214513aa --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/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; + +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/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocument.java index 41ce5d969a7..bae8f6f03b6 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocument.java @@ -1,5 +1,5 @@ // 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.impl.model; +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGenerator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGenerator.java index 55acf0b796c..4dd6881c07e 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGenerator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGenerator.java @@ -1,11 +1,11 @@ // 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.impl; +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument; +import com.google.inject.Inject; import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.IdentityDocument; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.ProviderUniqueId; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.SignedIdentityDocument; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.Allocation; @@ -29,8 +29,12 @@ public class IdentityDocumentGenerator { private final String providerDomain; private final int signingSecretVersion; - public IdentityDocumentGenerator(AthenzProviderServiceConfig config, AthenzProviderServiceConfig.Zones zoneConfig, - NodeRepository nodeRepository, Zone zone, KeyProvider keyProvider) { + @Inject + public IdentityDocumentGenerator(AthenzProviderServiceConfig config, + NodeRepository nodeRepository, + Zone zone, + KeyProvider keyProvider) { + AthenzProviderServiceConfig.Zones zoneConfig = Utils.getZoneConfig(config, zone); this.nodeRepository = nodeRepository; this.zone = zone; this.keyProvider = keyProvider; @@ -41,7 +45,7 @@ public class IdentityDocumentGenerator { this.signingSecretVersion = zoneConfig.secretVersion(); } - public String generateSignedIdentityDocument(String hostname) { + public SignedIdentityDocument generateSignedIdentityDocument(String hostname) { Node node = nodeRepository.getNode(hostname).orElseThrow(() -> new RuntimeException("Unable to find node " + hostname)); try { IdentityDocument identityDocument = generateIdDocument(node); @@ -51,13 +55,12 @@ public class IdentityDocumentGenerator { Base64.getEncoder().encodeToString(identityDocumentString.getBytes()); Signature sigGenerator = Signature.getInstance("SHA512withRSA"); - // TODO: Get the correct version 0 ok for now PrivateKey privateKey = keyProvider.getPrivateKey(signingSecretVersion); sigGenerator.initSign(privateKey); sigGenerator.update(encodedIdentityDocument.getBytes()); String signature = Base64.getEncoder().encodeToString(sigGenerator.sign()); - SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument( + return new SignedIdentityDocument( encodedIdentityDocument, signature, SignedIdentityDocument.DEFAULT_KEY_VERSION, @@ -65,9 +68,7 @@ public class IdentityDocumentGenerator { toZoneDnsSuffix(zone, dnsSuffix), providerDomain + "." + providerService, ztsUrl, - SignedIdentityDocument.DEFAILT_DOCUMENT_VERSION - ); - return Utils.getMapper().writeValueAsString(signedIdentityDocument); + SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION); } catch (Exception e) { throw new RuntimeException("Exception generating identity document: " + e.getMessage(), e); } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentResource.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentResource.java new file mode 100644 index 00000000000..b3e5aee97b3 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentResource.java @@ -0,0 +1,48 @@ +// 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.identitydocument; + +import com.google.inject.Inject; +import com.yahoo.container.jaxrs.annotation.Component; +import com.yahoo.log.LogLevel; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.GET; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import java.util.logging.Logger; + +/** + * @author bjorncs + */ +@Path("/identity-document") +public class IdentityDocumentResource { + + private static final Logger log = Logger.getLogger(IdentityDocumentResource.class.getName()); + + private final IdentityDocumentGenerator identityDocumentGenerator; + + @Inject + public IdentityDocumentResource(@Component IdentityDocumentGenerator identityDocumentGenerator) { + this.identityDocumentGenerator = identityDocumentGenerator; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public SignedIdentityDocument getIdentityDocument(@QueryParam("hostname") String hostname) { + // TODO Use TLS client authentication instead of blindly trusting hostname + if (hostname == null) { + throw new BadRequestException("The 'hostname' query parameter is missing"); + } + try { + return identityDocumentGenerator.generateSignedIdentityDocument(hostname); + } catch (Exception e) { + String message = String.format("Unable to generate identity doument for '%s': %s", hostname, e.getMessage()); + log.log(LogLevel.ERROR, message, e); + throw new InternalServerErrorException(message, e); + } + } + +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/ProviderUniqueId.java index 810c75ef0c5..1de2292d2d0 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/ProviderUniqueId.java @@ -1,5 +1,5 @@ // 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.impl.model; +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument; import com.fasterxml.jackson.annotation.JsonProperty; @@ -76,4 +76,4 @@ public class ProviderUniqueId { public int hashCode() { return Objects.hash(tenant, application, environment, region, instance, clusterId, clusterIndex); } -}
\ No newline at end of file +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/SignedIdentityDocument.java index 37f94d48a95..2545401f3ec 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/SignedIdentityDocument.java @@ -1,5 +1,5 @@ // 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.impl.model; +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -17,7 +17,7 @@ import java.util.Objects; public class SignedIdentityDocument { public static final int DEFAULT_KEY_VERSION = 0; - public static final int DEFAILT_DOCUMENT_VERSION = 1; + public static final int DEFAULT_DOCUMENT_VERSION = 1; @JsonProperty("identity-document")public final String rawIdentityDocument; @JsonIgnore public final IdentityDocument identityDocument; diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java index dab1581f580..c6aee673f9c 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java @@ -17,7 +17,7 @@ import java.util.concurrent.TimeUnit; /** * @author bjorncs */ -public class AthenzCertificateClient implements CertificateClient { +public class AthenzCertificateClient { private final AthenzProviderServiceConfig config; private final AthenzPrincipalAuthority authority; @@ -29,7 +29,6 @@ public class AthenzCertificateClient implements CertificateClient { this.zoneConfig = zoneConfig; } - @Override public X509Certificate updateCertificate(PrivateKey privateKey, TemporalAmount expiryTime) { SimpleServiceIdentityProvider identityProvider = new SimpleServiceIdentityProvider( authority, zoneConfig.domain(), zoneConfig.serviceName(), diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/CertificateClient.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/CertificateClient.java deleted file mode 100644 index 6465873e092..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/CertificateClient.java +++ /dev/null @@ -1,14 +0,0 @@ -// 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.impl; - -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.time.temporal.TemporalAmount; - -/** - * @author bjorncs - */ -@FunctionalInterface -public interface CertificateClient { - X509Certificate updateCertificate(PrivateKey privateKey, TemporalAmount expiryTime); -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java deleted file mode 100644 index 40a2a1dbcc9..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java +++ /dev/null @@ -1,44 +0,0 @@ -// 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.impl; - -import com.yahoo.athenz.auth.util.Crypto; - -import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.security.PrivateKey; -import java.security.PublicKey; - -/** - * @author bjorncs - */ -public class FileBackedKeyProvider implements KeyProvider { - - private final String keyPathPrefix; - - public FileBackedKeyProvider(String keyPathPrefix) { - this.keyPathPrefix = keyPathPrefix; - } - - @Override - public PrivateKey getPrivateKey(int version) { - return Crypto.loadPrivateKey(readPemStringFromFile(new File(keyPathPrefix + ".priv." + version))); - } - - @Override - public PublicKey getPublicKey(int version) { - return Crypto.loadPublicKey(readPemStringFromFile(new File(keyPathPrefix + ".pub." + version))); - } - - private static String readPemStringFromFile(File file) { - try { - if (!file.exists() || !file.isFile()) { - throw new IllegalArgumentException("Key missing: " + file.getAbsolutePath()); - } - return new String(Files.readAllBytes(file.toPath())); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentServlet.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentServlet.java deleted file mode 100644 index a66fdf9d82f..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentServlet.java +++ /dev/null @@ -1,51 +0,0 @@ -// 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.impl; - -import com.yahoo.log.LogLevel; - -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.io.PrintWriter; -import java.util.logging.Logger; - -/** - * @author bjorncs - */ -public class IdentityDocumentServlet extends HttpServlet { - - private static final Logger log = Logger.getLogger(IdentityDocumentServlet.class.getName()); - - private final IdentityDocumentGenerator identityDocumentGenerator; - - public IdentityDocumentServlet(IdentityDocumentGenerator identityDocumentGenerator) { - this.identityDocumentGenerator = identityDocumentGenerator; - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - // TODO verify tls client cert - String hostname = req.getParameter("hostname"); - if (hostname == null) { - String message = "The 'hostname' parameter is missing"; - log.log(LogLevel.ERROR, message); - resp.sendError(HttpServletResponse.SC_BAD_REQUEST, message); - return; - } - try { - log.log(LogLevel.INFO, "Generating identity document for " + hostname); - String signedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(hostname); - resp.setContentType("application/json"); - PrintWriter writer = resp.getWriter(); - writer.print(signedIdentityDocument); - writer.flush(); - } catch (Exception e) { - String message = String.format("Unable to generate identity doument [%s]", e.getMessage()); - log.log(LogLevel.ERROR, message); - resp.sendError(HttpServletResponse.SC_NOT_FOUND, message); - } - } - -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceConfirmationServlet.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceConfirmationServlet.java deleted file mode 100644 index 766b95b443b..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceConfirmationServlet.java +++ /dev/null @@ -1,65 +0,0 @@ -// 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.impl; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.yahoo.log.LogLevel; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.InstanceConfirmation; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.Reader; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * A Servlet implementing the Athenz Service Provider InstanceConfirmation API - * - * @author bjorncs - */ -public class InstanceConfirmationServlet extends HttpServlet { - - private static final Logger log = Logger.getLogger(InstanceConfirmationServlet.class.getName()); - - private final InstanceValidator instanceValidator; - - public InstanceConfirmationServlet(InstanceValidator instanceValidator) { - this.instanceValidator = instanceValidator; - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - // TODO Validate that request originates from ZTS - try { - String confirmationContent = toString(req.getReader()); - log.log(LogLevel.DEBUG, () -> "Confirmation content: " + confirmationContent); - InstanceConfirmation instanceConfirmation = - Utils.getMapper().readValue(confirmationContent, InstanceConfirmation.class); - log.log(LogLevel.DEBUG, () -> "Parsed confirmation content: " + instanceConfirmation.toString()); - if (!instanceValidator.isValidInstance(instanceConfirmation)) { - String message = "Invalid instance: " + instanceConfirmation; - log.log(LogLevel.ERROR, message); - resp.sendError(HttpServletResponse.SC_FORBIDDEN, message); - } else { - resp.setStatus(HttpServletResponse.SC_OK); - resp.setContentType("application/json"); - resp.getWriter().write(Utils.getMapper().writeValueAsString(instanceConfirmation)); - } - } catch (JsonParseException | JsonMappingException e) { - String message = "InstanceConfirmation is not valid JSON"; - log.log(LogLevel.ERROR, message, e); - resp.sendError(HttpServletResponse.SC_BAD_REQUEST, message); - } - } - - private static String toString(Reader reader) throws IOException { - try (BufferedReader bufferedReader = new BufferedReader(reader)) { - return bufferedReader.lines().collect(Collectors.joining("\n")); - } - } - -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/SecretStoreKeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/SecretStoreKeyProvider.java index 93abda1f9ea..e66131b6cf7 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/SecretStoreKeyProvider.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/SecretStoreKeyProvider.java @@ -1,8 +1,12 @@ // 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.impl; +import com.google.inject.Inject; import com.yahoo.athenz.auth.util.Crypto; +import com.yahoo.config.provision.Zone; import com.yahoo.jdisc.http.SecretStore; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; import java.security.KeyPair; import java.security.PrivateKey; @@ -10,19 +14,24 @@ import java.security.PublicKey; import java.util.HashMap; import java.util.Map; +import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils.getZoneConfig; + /** * @author mortent */ +@SuppressWarnings("unused") // Injected component public class SecretStoreKeyProvider implements KeyProvider { private final SecretStore secretStore; private final String secretName; private final Map<Integer, KeyPair> secrets; - - public SecretStoreKeyProvider(SecretStore secretStore, String secretName) { + @Inject + public SecretStoreKeyProvider(SecretStore secretStore, + Zone zone, + AthenzProviderServiceConfig config) { this.secretStore = secretStore; - this.secretName = secretName; + this.secretName = getZoneConfig(config, zone).secretName(); this.secrets = new HashMap<>(); } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java deleted file mode 100644 index fd5ba5843aa..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java +++ /dev/null @@ -1,21 +0,0 @@ -// 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.impl; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * A simple status servlet that should return status code 200 as long as the provider service servlet is up. - * - * @author bjorncs - */ -public class StatusServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - resp.setStatus(HttpServletResponse.SC_OK); - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java index d81ec183fd4..ad54aa341bf 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java @@ -3,6 +3,8 @@ package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; /** * @author bjorncs @@ -20,4 +22,10 @@ public class Utils { mapper.registerModule(new JavaTimeModule()); return mapper; } + + public static AthenzProviderServiceConfig.Zones getZoneConfig(AthenzProviderServiceConfig config, Zone zone) { + String key = zone.environment().value() + "." + zone.region().value(); + return config.zones(key); + } + } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceConfirmation.java index ade42968e58..7b2725a8d95 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceConfirmation.java @@ -1,5 +1,5 @@ // 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.impl.model; +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument.SignedIdentityDocument; import java.io.IOException; import java.util.HashMap; diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceConfirmationResource.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceConfirmationResource.java new file mode 100644 index 00000000000..5c93bf423d3 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceConfirmationResource.java @@ -0,0 +1,41 @@ +// 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.instanceconfirmation; + +import com.google.inject.Inject; +import com.yahoo.container.jaxrs.annotation.Component; +import com.yahoo.log.LogLevel; + +import javax.ws.rs.Consumes; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.util.logging.Logger; + +/** + * @author bjorncs + */ +@Path("/instance") +public class InstanceConfirmationResource { + + private static final Logger log = Logger.getLogger(InstanceConfirmationResource.class.getName()); + + private final InstanceValidator instanceValidator; + + @Inject + public InstanceConfirmationResource(@Component InstanceValidator instanceValidator) { + this.instanceValidator = instanceValidator; + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public InstanceConfirmation confirmInstance(InstanceConfirmation instanceConfirmation) { + if (!instanceValidator.isValidInstance(instanceConfirmation)) { + log.log(LogLevel.ERROR, "Invalid instance: " + instanceConfirmation); + throw new ForbiddenException("Instance is invalid"); + } + return instanceConfirmation; + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java index 427f35c41d8..69c5d961b7e 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java @@ -1,14 +1,15 @@ // 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.impl; +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation; +import com.google.inject.Inject; import com.yahoo.config.model.api.ApplicationInfo; import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.model.api.SuperModelProvider; import com.yahoo.config.provision.ApplicationId; import com.yahoo.log.LogLevel; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.InstanceConfirmation; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.ProviderUniqueId; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.SignedIdentityDocument; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument.ProviderUniqueId; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument.SignedIdentityDocument; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -33,6 +34,7 @@ public class InstanceValidator { private final KeyProvider keyProvider; private final SuperModelProvider superModelProvider; + @Inject public InstanceValidator(KeyProvider keyProvider, SuperModelProvider superModelProvider) { this.keyProvider = keyProvider; this.superModelProvider = superModelProvider; @@ -64,7 +66,7 @@ public class InstanceValidator { return isSignatureValid(publicKey, signedIdentityDocument.rawIdentityDocument, signedIdentityDocument.signature); } - static boolean isSignatureValid(PublicKey publicKey, String rawIdentityDocument, String signature) { + public static boolean isSignatureValid(PublicKey publicKey, String rawIdentityDocument, String signature) { try { Signature signatureVerifier = Signature.getInstance("SHA512withRSA"); signatureVerifier.initVerify(publicKey); diff --git a/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def b/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def index 4aad9a4eae2..13cc78b0bd0 100644 --- a/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def +++ b/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def @@ -13,12 +13,6 @@ zones{}.secretName string # Secret version zones{}.secretVersion int -# HTTPS port for Athenz Provider Service endpoint -port int default=8443 - -# InstanceConfirmation API path -apiPath string default="/athenz/v1/provider" - # Athenz principal authority header name athenzPrincipalHeaderName string default="Athenz-Principal-Auth" 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 deleted file mode 100644 index bf0746aee7e..00000000000 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java +++ /dev/null @@ -1,270 +0,0 @@ -// 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; - -import athenz.shade.zts.jersey.repackaged.com.google.common.collect.ImmutableMap; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.yahoo.config.provision.Environment; -import com.yahoo.config.provision.RegionName; -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.config.AthenzProviderServiceConfig; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.CertificateClient; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.IdentityDocumentGenerator; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.InstanceValidator; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.KeyProvider; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.IdentityDocument; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.InstanceConfirmation; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.ProviderUniqueId; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.SignedIdentityDocument; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.ssl.SSLContextBuilder; -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x509.BasicConstraints; -import org.bouncycastle.cert.CertIOException; -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.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.junit.Test; - -import javax.net.ssl.SSLContext; -import java.io.IOException; -import java.math.BigInteger; -import java.security.KeyManagementException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.Signature; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.time.Instant; -import java.time.temporal.TemporalAmount; -import java.util.Base64; -import java.util.Calendar; -import java.util.Date; -import java.util.concurrent.ScheduledExecutorService; -import java.util.logging.Logger; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyLong; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author bjorncs - */ -public class AthenzInstanceProviderServiceTest { - - private static final Logger log = Logger.getLogger(AthenzInstanceProviderServiceTest.class.getName()); - private static final int PORT = 12345; - private static final Zone ZONE = new Zone(SystemName.cd, Environment.dev, RegionName.from("us-north-1")); - - @Test - public void provider_service_hosts_endpoint_secured_with_tls() throws Exception { - String domain = "domain"; - String service = "service"; - - AutoGeneratedKeyProvider keyProvider = new AutoGeneratedKeyProvider(); - PrivateKey privateKey = keyProvider.getPrivateKey(0); - AthenzProviderServiceConfig config = getAthenzProviderConfig(domain, service, "vespa.dns.suffix", ZONE); - SslContextFactory sslContextFactory = AthenzInstanceProviderService.createSslContextFactory(); - AthenzCertificateUpdater certificateUpdater = new AthenzCertificateUpdater( - new SelfSignedCertificateClient(keyProvider.getKeyPair(), config, getZoneConfig(config, ZONE)), - sslContextFactory, - keyProvider, - config, - getZoneConfig(config, ZONE)); - - ScheduledExecutorService executor = mock(ScheduledExecutorService.class); - when(executor.awaitTermination(anyLong(), any())).thenReturn(true); - - 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); - - try (CloseableHttpClient client = createHttpClient(domain, service)) { - assertFalse(getStatus(client)); - certificateUpdater.run(); - assertTrue(getStatus(client)); - assertInstanceConfirmationSucceeds(client, privateKey); - certificateUpdater.run(); - assertTrue(getStatus(client)); - assertInstanceConfirmationSucceeds(client, privateKey); - } finally { - athenzInstanceProviderService.deconstruct(); - } - } - - public static AthenzProviderServiceConfig getAthenzProviderConfig(String domain, String service, String dnsSuffix, Zone zone) { - AthenzProviderServiceConfig.Zones.Builder zoneConfig = - new AthenzProviderServiceConfig.Zones.Builder() - .serviceName(service) - .secretVersion(0) - .domain(domain) - .secretName("s3cr3t"); - - return new AthenzProviderServiceConfig( - new AthenzProviderServiceConfig.Builder() - .zones(ImmutableMap.of(zone.environment().value() + "." + zone.region().value(), zoneConfig)) - .port(PORT) - .certDnsSuffix(dnsSuffix) - .ztsUrl("localhost/zts") - .athenzPrincipalHeaderName("Athenz-Principal-Auth") - .apiPath("")); - - } - - public static AthenzProviderServiceConfig.Zones getZoneConfig(AthenzProviderServiceConfig config, Zone zone) { - return config.zones(zone.environment().value() + "." + zone.region().value()); - } - - private static boolean getStatus(HttpClient client) { - try { - HttpResponse response = client.execute(new HttpGet("https://localhost:" + PORT + "/status.html")); - return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK; - } catch (Exception e) { - log.log(LogLevel.INFO, "Status.html failed: " + e); - return false; - } - } - - private static void assertInstanceConfirmationSucceeds(HttpClient client, PrivateKey privateKey) throws IOException { - HttpPost httpPost = new HttpPost("https://localhost:" + PORT + "/instance"); - httpPost.setEntity(createInstanceConfirmation(privateKey)); - HttpResponse response = client.execute(httpPost); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - } - - private static CloseableHttpClient createHttpClient(String domain, String service) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { - SSLContext sslContext = new SSLContextBuilder() - .loadTrustMaterial(null, (certificateChain, ignoredAuthType) -> - certificateChain[0].getSubjectX500Principal().getName().equals("CN=" + domain + "." + service)) - .build(); - - return HttpClients.custom() - .setSslcontext(sslContext) - .setSSLHostnameVerifier(new NoopHostnameVerifier()) - .build(); - } - - private static HttpEntity createInstanceConfirmation(PrivateKey privateKey) { - IdentityDocument identityDocument = new IdentityDocument( - new ProviderUniqueId("tenant", "application", "environment", "region", "instance", "cluster-id", 0), - "hostname", - "instance-hostname", - Instant.now()); - try { - ObjectMapper mapper = Utils.getMapper(); - String encodedIdentityDocument = - Base64.getEncoder().encodeToString(mapper.writeValueAsString(identityDocument).getBytes()); - Signature sigGenerator = Signature.getInstance("SHA512withRSA"); - sigGenerator.initSign(privateKey); - sigGenerator.update(encodedIdentityDocument.getBytes()); - - InstanceConfirmation instanceConfirmation = new InstanceConfirmation( - "provider", "domain", "service", - new SignedIdentityDocument(encodedIdentityDocument, - Base64.getEncoder().encodeToString(sigGenerator.sign()), - 0, - identityDocument.providerUniqueId.asString(), - "dnssuffix", - "service", - "localhost/zts", - 1)); - return new StringEntity(mapper.writeValueAsString(instanceConfirmation)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public static class AutoGeneratedKeyProvider implements KeyProvider { - - private final KeyPair keyPair; - - public AutoGeneratedKeyProvider() { - try { - KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA"); - rsa.initialize(2048); - keyPair = rsa.genKeyPair(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - @Override - public PrivateKey getPrivateKey(int version) { - return keyPair.getPrivate(); - } - - @Override - public PublicKey getPublicKey(int version) { - return keyPair.getPublic(); - } - - public KeyPair getKeyPair() { - return keyPair; - } - } - - private static class SelfSignedCertificateClient implements CertificateClient { - - private final KeyPair keyPair; - private final AthenzProviderServiceConfig config; - private final AthenzProviderServiceConfig.Zones zoneConfig; - - private SelfSignedCertificateClient(KeyPair keyPair, AthenzProviderServiceConfig config, - AthenzProviderServiceConfig.Zones zoneConfig) { - this.keyPair = keyPair; - this.config = config; - this.zoneConfig = zoneConfig; - } - - @Override - public X509Certificate updateCertificate(PrivateKey privateKey, TemporalAmount expiryTime) { - try { - ContentSigner contentSigner = new JcaContentSignerBuilder("SHA512WithRSA").build(keyPair.getPrivate()); - X500Name dnName = new X500Name("CN=" + zoneConfig.domain() + "." + zoneConfig.serviceName()); - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.HOUR, 1); - Date endDate = calendar.getTime(); - JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( - dnName, BigInteger.ONE, new Date(), endDate, dnName, keyPair.getPublic()); - certBuilder.addExtension(new ASN1ObjectIdentifier("2.5.29.19"), true, new BasicConstraints(true)); - - return new JcaX509CertificateConverter() - .setProvider(new BouncyCastleProvider()) - .getCertificate(certBuilder.build(contentSigner)); - } catch (CertificateException | CertIOException | OperatorCreationException e) { - throw new RuntimeException(e); - } - } - } -} diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AutoGeneratedKeyProvider.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AutoGeneratedKeyProvider.java new file mode 100644 index 00000000000..ca6b5529b08 --- /dev/null +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AutoGeneratedKeyProvider.java @@ -0,0 +1,40 @@ +// 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; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * @author bjorncs + */ +public class AutoGeneratedKeyProvider implements KeyProvider { + + private final KeyPair keyPair; + + public AutoGeneratedKeyProvider() { + try { + KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA"); + rsa.initialize(2048); + keyPair = rsa.genKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + public PrivateKey getPrivateKey(int version) { + return keyPair.getPrivate(); + } + + @Override + public PublicKey getPublicKey(int version) { + return keyPair.getPublic(); + } + + public KeyPair getKeyPair() { + return keyPair; + } +} diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/TestUtils.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/TestUtils.java new file mode 100644 index 00000000000..c09a9fb1740 --- /dev/null +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/TestUtils.java @@ -0,0 +1,31 @@ +// 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; + +import com.google.common.collect.ImmutableMap; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; + +/** + * @author bjorncs + */ +public class TestUtils { + + public static AthenzProviderServiceConfig getAthenzProviderConfig(String domain, + String service, + String dnsSuffix, + Zone zone) { + AthenzProviderServiceConfig.Zones.Builder zoneConfig = + new AthenzProviderServiceConfig.Zones.Builder() + .serviceName(service) + .secretVersion(0) + .domain(domain) + .secretName("s3cr3t"); + return new AthenzProviderServiceConfig( + new AthenzProviderServiceConfig.Builder() + .zones(ImmutableMap.of(zone.environment().value() + "." + zone.region().value(), zoneConfig)) + .certDnsSuffix(dnsSuffix) + .ztsUrl("localhost/zts") + .athenzPrincipalHeaderName("Athenz-Principal-Auth")); + } + +} 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); + } + } +} diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CsrSerializedPayloadTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CsrSerializedPayloadTest.java new file mode 100644 index 00000000000..b8433856f95 --- /dev/null +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CsrSerializedPayloadTest.java @@ -0,0 +1,32 @@ +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca; + +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils; +import org.junit.Test; + +import java.io.IOException; + +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +/** + * @author bjorncs + */ +public class CsrSerializedPayloadTest { + + @Test + public void it_can_be_deserialized() throws IOException { + String serialized = "{\"csr\":\"-----BEGIN CERTIFICATE REQUEST-----\\nMIICVDCCATwCAQAwDzENMAsGA1UEAwwEdGV" + + "zdDCCASIwDQYJKoZIhvcNAQEBBQAD\\nggEPADCCAQoCggEBAL7xra4De9B54yY6lw8Ka/lt7lDEKQRp42RYzpXjHIQXFgr8" + + "\\n+EvJCLEldFoqfOm728KAWQq/8YdFR4hBwOz8Rr8khJKMBCQ2DWvGYz2705nr3j3v\\nsd3RE5i8n8cUdKiHRuOf305xgy" + + "970TFb+s5/tQOfDMDfvC/BdHNhB4pc0P04CVs/\\nzusKvghdSXFVufAuVaY30ZyviqrDVlBZnI158MmRzfINwP70ZYn5wsq" + + "crKzgSUBp\\nH/WjxaklSzGOH8Uk/EKVx0luzAxtTU8jO7MU1+EG8H4E+FI9ijdjftYyko5UAOQO\\nJGiI9/qHJIMVOIcQa" + + "k1PA5+2/0NbtVxihQi/uJcCAwEAAaAAMA0GCSqGSIb3DQEB\\nCwUAA4IBAQAelFvM6PyDFufv9pNmFigNqOO+r8ats9Xak9" + + "JVtGERo9KFcNDAkawD\\nMPzWQeB87oPnB5dlSdkI2J/jIV7/zR9Qoa2qZlKeL4vUIvfMTj5EOmQLn4ofoBwa\\n50D8Ro3D" + + "06Ohb1KE3seOK2FfVybiATpoaICCjb0ibhx4lNsJGZXpw6F2OdTRi8Fb\\n7kfgLiLPCH+UiHDeVnjVVr/PUKeSImgv44mb4" + + "c6EU29MYkM4LxCY9/c4scG7Pq+s\\nuHU5Tepjsnmkdtip5NzS7csPXENEygKyksPHWFFojPrtF6nFkMzzIPUgKbsmm4+H\\" + + "nfJihCYL3pc3+bVYl87TIcdohJ1GYvfw7\\n-----END CERTIFICATE REQUEST-----\\n\"}"; + CsrSerializedPayload csrSerializedPayload = Utils.getMapper().readValue(serialized, CsrSerializedPayload.class); + assertThat(csrSerializedPayload.csr, notNullValue()); + } + +} diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGeneratorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGeneratorTest.java index d77757374ce..0c12e137e27 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGeneratorTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGeneratorTest.java @@ -1,4 +1,4 @@ -package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument; import com.google.common.collect.ImmutableSet; @@ -13,10 +13,9 @@ import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.AthenzInstanceProviderServiceTest.AutoGeneratedKeyProvider; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.AutoGeneratedKeyProvider; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.ProviderUniqueId; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.SignedIdentityDocument; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceValidator; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.Allocation; @@ -27,8 +26,7 @@ import org.junit.Test; import java.util.HashSet; import java.util.Optional; -import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.AthenzInstanceProviderServiceTest.getAthenzProviderConfig; -import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.AthenzInstanceProviderServiceTest.getZoneConfig; +import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.TestUtils.getAthenzProviderConfig; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.eq; @@ -66,17 +64,9 @@ public class IdentityDocumentGeneratorTest { String dnsSuffix = "vespa.dns.suffix"; AthenzProviderServiceConfig config = getAthenzProviderConfig("domain", "service", dnsSuffix, ZONE); - IdentityDocumentGenerator identityDocumentGenerator = new IdentityDocumentGenerator( - config, - getZoneConfig(config, ZONE), - nodeRepository, - ZONE, - keyProvider); - String rawSignedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(hostname); - - - SignedIdentityDocument signedIdentityDocument = - Utils.getMapper().readValue(rawSignedIdentityDocument, SignedIdentityDocument.class); + IdentityDocumentGenerator identityDocumentGenerator = + new IdentityDocumentGenerator(config, nodeRepository, ZONE, keyProvider); + SignedIdentityDocument signedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(hostname); // Verify attributes assertEquals(hostname, signedIdentityDocument.identityDocument.instanceHostname); @@ -92,7 +82,7 @@ public class IdentityDocumentGeneratorTest { // Validate signature assertTrue("Message", InstanceValidator.isSignatureValid(keyProvider.getPublicKey(0), - signedIdentityDocument.rawIdentityDocument, - signedIdentityDocument.signature)); + signedIdentityDocument.rawIdentityDocument, + signedIdentityDocument.signature)); } -}
\ No newline at end of file +} diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidatorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java index c1fab319ebf..c68a8805abc 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidatorTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java @@ -1,4 +1,4 @@ -package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation; import com.fasterxml.jackson.databind.ObjectMapper; import com.yahoo.config.model.api.ApplicationInfo; @@ -8,11 +8,12 @@ import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.model.api.SuperModel; import com.yahoo.config.model.api.SuperModelProvider; import com.yahoo.config.provision.ApplicationId; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.AthenzInstanceProviderServiceTest.AutoGeneratedKeyProvider; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.IdentityDocument; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.InstanceConfirmation; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.ProviderUniqueId; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.SignedIdentityDocument; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.AutoGeneratedKeyProvider; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument.IdentityDocument; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument.ProviderUniqueId; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.identitydocument.SignedIdentityDocument; import org.junit.Test; import java.security.PrivateKey; @@ -28,8 +29,8 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; -import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.InstanceValidator.SERVICE_PROPERTIES_DOMAIN_KEY; -import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.InstanceValidator.SERVICE_PROPERTIES_SERVICE_KEY; +import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceValidator.SERVICE_PROPERTIES_DOMAIN_KEY; +import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceValidator.SERVICE_PROPERTIES_SERVICE_KEY; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -168,4 +169,4 @@ public class InstanceValidatorTest { return new ApplicationInfo(appId, 0, model); } -}
\ No newline at end of file +} |