summaryrefslogtreecommitdiffstats
path: root/athenz-identity-provider-service
diff options
context:
space:
mode:
Diffstat (limited to 'athenz-identity-provider-service')
-rw-r--r--athenz-identity-provider-service/OWNERS1
-rw-r--r--athenz-identity-provider-service/pom.xml122
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java174
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java40
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java56
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java11
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java61
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java21
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java23
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java70
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java99
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.java68
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java72
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java8
-rw-r--r--athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def29
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java163
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java115
17 files changed, 1133 insertions, 0 deletions
diff --git a/athenz-identity-provider-service/OWNERS b/athenz-identity-provider-service/OWNERS
new file mode 100644
index 00000000000..569bf1cc3a1
--- /dev/null
+++ b/athenz-identity-provider-service/OWNERS
@@ -0,0 +1 @@
+bjorncs
diff --git a/athenz-identity-provider-service/pom.xml b/athenz-identity-provider-service/pom.xml
new file mode 100644
index 00000000000..e3998b02ad0
--- /dev/null
+++ b/athenz-identity-provider-service/pom.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>athenz-identity-provider-service</artifactId>
+ <packaging>container-plugin</packaging>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ </parent>
+ <dependencies>
+ <!-- COMPILE -->
+ <dependency>
+ <groupId>com.yahoo.athenz</groupId>
+ <artifactId>athenz-zms-java-client</artifactId>
+ <scope>compile</scope>
+ <exclusions>
+ <!--Exclude all Jersey bundles provided by JDisc-->
+ <exclusion>
+ <groupId>org.glassfish.jersey.core</groupId>
+ <artifactId>jersey-client</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-json-jackson</artifactId>
+ </exclusion>
+ <!-- BouncyCastle is not bundled due to class loading issues
+ when security provider is registered from inside a OSGi bundle -->
+ <exclusion>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.athenz</groupId>
+ <artifactId>athenz-zts-java-client</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jsr310</artifactId>
+ <scope>compile</scope>
+ </dependency>
+
+ <!-- PROVIDED -->
+ <!-- BouncyCastle should be available through jdisc_http_service at runtime -->
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-dev</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-servlet</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- TEST -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-test</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>4.4.1</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore</artifactId>
+ <version>4.4.1</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
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
new file mode 100644
index 00000000000..e3b31263421
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java
@@ -0,0 +1,174 @@
+// 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.athenz.auth.impl.PrincipalAuthority;
+import com.yahoo.athenz.auth.impl.SimpleServiceIdentityProvider;
+import com.yahoo.athenz.auth.util.Crypto;
+import com.yahoo.athenz.zts.InstanceRefreshRequest;
+import com.yahoo.athenz.zts.ZTSClient;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.jdisc.http.ssl.ReaderForPath;
+import com.yahoo.jdisc.http.ssl.pem.PemKeyStore;
+import com.yahoo.jdisc.http.ssl.pem.PemSslKeyStore;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.hosted.athenz.identityproviderservice.config.AthenzProviderServiceConfig;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.FileBackedKeyProvider;
+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.ProviderServiceServlet;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.StatusServlet;
+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.io.StringReader;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+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) {
+ this(config, new FileBackedKeyProvider(config.keyPathPrefix()), Executors.newSingleThreadScheduledExecutor());
+ }
+
+ AthenzInstanceProviderService(AthenzProviderServiceConfig config,
+ KeyProvider keyProvider,
+ ScheduledExecutorService scheduler) {
+ this.scheduler = scheduler;
+ SslContextFactory sslContextFactory = createSslContextFactory();
+ this.jetty = createJettyServer(config.port(), config.apiPath(), keyProvider, sslContextFactory);
+ AthenzCertificateUpdater reloader = new AthenzCertificateUpdater(
+ sslContextFactory, keyProvider, config);
+ scheduler.scheduleAtFixedRate(reloader, 0, 1, TimeUnit.DAYS);
+ try {
+ jetty.start();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Server createJettyServer(int port, String apiPath,
+ KeyProvider keyProvider, SslContextFactory sslContextFactory) {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server, sslContextFactory);
+ connector.setPort(port);
+ server.addConnector(connector);
+
+ ServletHandler handler = new ServletHandler();
+ ProviderServiceServlet providerServiceServlet =
+ new ProviderServiceServlet(new InstanceValidator(keyProvider));
+ handler.addServletWithMapping(new ServletHolder(providerServiceServlet), apiPath);
+ handler.addServletWithMapping(StatusServlet.class, "/status.html");
+ server.setHandler(handler);
+ return server;
+
+ }
+
+ private 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);
+ }
+ }
+
+ private static class AthenzCertificateUpdater implements Runnable {
+
+ private static final Logger log = Logger.getLogger(AthenzCertificateUpdater.class.getName());
+
+ private final SslContextFactory sslContextFactory;
+ private final KeyProvider keyProvider;
+ private final AthenzProviderServiceConfig config;
+
+ private AthenzCertificateUpdater(SslContextFactory sslContextFactory,
+ KeyProvider keyProvider,
+ AthenzProviderServiceConfig config) {
+ this.sslContextFactory = sslContextFactory;
+ this.keyProvider = keyProvider;
+ this.config = config;
+ }
+
+ @Override
+ public void run() {
+ try {
+ log.log(LogLevel.INFO, "Updating Athenz certificate through ZTS");
+ String privateKey = keyProvider.getPrivateKey(config.keyVersion());
+ String certificate = getCertificateFromZTS(Crypto.loadPrivateKey(privateKey));
+ final KeyStore keyStore =
+ new PemSslKeyStore(
+ new PemKeyStore.KeyStoreLoadParameter(
+ new ReaderForPath(new StringReader(certificate), null),
+ new ReaderForPath(new StringReader(privateKey), null)))
+ .loadJavaKeyStore();
+ sslContextFactory.reload(sslContextFactory -> sslContextFactory.setKeyStore(keyStore));
+ log.log(LogLevel.INFO, "Athenz certificate reload successfully completed");
+ } catch (Exception e) {
+ log.log(LogLevel.ERROR, "Failed to update certificate from ZTS: " + e.getMessage(), e);
+ }
+ }
+
+ private String getCertificateFromZTS(PrivateKey privateKey) {
+ SimpleServiceIdentityProvider identityProvider = new SimpleServiceIdentityProvider(
+ new AthenzPrincipalAuthority(config.athenzPrincipalHeaderName()), config.domain(), config.serviceName(),
+ privateKey, Integer.toString(config.keyVersion()), TimeUnit.MINUTES.toSeconds(10));
+ ZTSClient ztsClient = new ZTSClient(
+ config.ztsUrl(), config.domain(), config.serviceName(), identityProvider);
+ InstanceRefreshRequest req = ZTSClient.generateInstanceRefreshRequest(
+ config.domain(), config.serviceName(), privateKey, config.certDnsSuffix(), (int)TimeUnit.DAYS.toSeconds(30));
+ return ztsClient.postInstanceRefreshRequest(config.domain(), config.serviceName(), req).getCertificate();
+ }
+
+ private static class AthenzPrincipalAuthority extends PrincipalAuthority {
+ private final String headerName;
+
+ public AthenzPrincipalAuthority(String headerName) {
+ this.headerName = headerName;
+ }
+
+ @Override
+ public String getHeader() {
+ return headerName;
+ }
+ }
+ }
+
+ @Override
+ public void deconstruct() {
+ try {
+ log.log(LogLevel.INFO, "Deconstructing Athenz provider service");
+ scheduler.shutdown();
+ jetty.stop();
+ if (!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/impl/FileBackedKeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java
new file mode 100644
index 00000000000..f03f8415586
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.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.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+
+/**
+ * @author bjorncs
+ */
+public class FileBackedKeyProvider implements KeyProvider {
+
+ private final String keyPathPrefix;
+
+ public FileBackedKeyProvider(String keyPathPrefix) {
+ this.keyPathPrefix = keyPathPrefix;
+ }
+
+ @Override
+ public String getPrivateKey(int version) {
+ return loadKey(new File(keyPathPrefix + ".priv." + version));
+ }
+
+ @Override
+ public String getPublicKey(int version) {
+ return loadKey(new File(keyPathPrefix + ".pub." + version));
+ }
+
+ private static String loadKey(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/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java
new file mode 100644
index 00000000000..da8a4afebd8
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java
@@ -0,0 +1,56 @@
+// 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 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 java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.util.Base64;
+import java.util.logging.Logger;
+
+/**
+ * Verifies that the instance's identity document is valid
+ *
+ * @author bjorncs
+ */
+public class InstanceValidator {
+
+ private static final Logger log = Logger.getLogger(InstanceValidator.class.getName());
+
+ private final KeyProvider keyProvider;
+
+ public InstanceValidator(KeyProvider keyProvider) {
+ this.keyProvider = keyProvider;
+ }
+
+ public boolean isValidInstance(InstanceConfirmation instanceConfirmation) {
+ SignedIdentityDocument signedIdentityDocument = instanceConfirmation.signedIdentityDocument;
+ ProviderUniqueId providerUniqueId = signedIdentityDocument.identityDocument.providerUniqueId;
+ log.log(LogLevel.INFO, () -> String.format("Validating instance %s.", providerUniqueId));
+ PublicKey publicKey = Crypto.loadPublicKey(keyProvider.getPublicKey(signedIdentityDocument.signingKeyVersion));
+ if (isSignatureValid(publicKey, signedIdentityDocument.rawIdentityDocument, signedIdentityDocument.signature)) {
+ log.log(LogLevel.INFO, () -> String.format("Instance %s is valid.", providerUniqueId));
+ return true;
+ }
+ log.log(LogLevel.ERROR, () -> String.format("Instance %s has invalid signature.", providerUniqueId));
+ return false;
+ }
+
+ private static boolean isSignatureValid(PublicKey publicKey, String rawIdentityDocument, String signature) {
+ try {
+ Signature signatureVerifier = Signature.getInstance("SHA512withRSA");
+ signatureVerifier.initVerify(publicKey);
+ signatureVerifier.update(rawIdentityDocument.getBytes());
+ return signatureVerifier.verify(Base64.getDecoder().decode(signature));
+ } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
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/impl/KeyProvider.java
new file mode 100644
index 00000000000..8c807405693
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java
@@ -0,0 +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;
+
+/**
+ * @author bjorncs
+ */
+public interface KeyProvider {
+ String getPrivateKey(int version);
+
+ String getPublicKey(int version);
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java
new file mode 100644
index 00000000000..a3a4d97706d
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java
@@ -0,0 +1,61 @@
+// 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 ProviderServiceServlet extends HttpServlet {
+
+ private static final Logger log = Logger.getLogger(ProviderServiceServlet.class.getName());
+
+ private final InstanceValidator instanceValidator;
+
+ public ProviderServiceServlet(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)) {
+ log.log(LogLevel.ERROR, "Invalid instance: " + instanceConfirmation);
+ resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ } else {
+ resp.setStatus(HttpServletResponse.SC_OK);
+ }
+ } catch (JsonParseException | JsonMappingException e) {
+ log.log(LogLevel.ERROR, "InstanceConfirmation is not valid JSON", e);
+ resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ }
+ }
+
+ 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/StatusServlet.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java
new file mode 100644
index 00000000000..fd5ba5843aa
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java
@@ -0,0 +1,21 @@
+// 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
new file mode 100644
index 00000000000..d81ec183fd4
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java
@@ -0,0 +1,23 @@
+// 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.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+/**
+ * @author bjorncs
+ */
+public class Utils {
+
+ private static final ObjectMapper mapper = createObjectMapper();
+
+ public static ObjectMapper getMapper() {
+ return mapper;
+ }
+
+ private static ObjectMapper createObjectMapper() {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ return mapper;
+ }
+}
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/impl/model/IdentityDocument.java
new file mode 100644
index 00000000000..0b4fc38b00d
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java
@@ -0,0 +1,70 @@
+// 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;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * @author bjorncs
+ */
+public class IdentityDocument {
+
+ @JsonProperty("athens-domain")
+ public final String athensDomain;
+ @JsonProperty("athens-service")
+ public final String athensService;
+ @JsonProperty("provider-unique-id")
+ public final ProviderUniqueId providerUniqueId;
+ @JsonProperty("configserver-hostname")
+ public final String configServerHostname;
+ @JsonProperty("instance-hostname")
+ public final String instanceHostname;
+ @JsonProperty("created-at")
+ public final Instant createdAt;
+
+ public IdentityDocument(@JsonProperty("athens-domain") String athensDomain,
+ @JsonProperty("athens-service") String athensService,
+ @JsonProperty("provider-unique-id") ProviderUniqueId providerUniqueId,
+ @JsonProperty("configserver-hostname") String configServerHostname,
+ @JsonProperty("instance-hostname") String instanceHostname,
+ @JsonProperty("created-at") Instant createdAt) {
+ this.athensDomain = athensDomain;
+ this.athensService = athensService;
+ this.providerUniqueId = providerUniqueId;
+ this.configServerHostname = configServerHostname;
+ this.instanceHostname = instanceHostname;
+ this.createdAt = createdAt;
+ }
+
+ @Override
+ public String toString() {
+ return "IdentityDocument{" +
+ "athensDomain='" + athensDomain + '\'' +
+ ", athensService='" + athensService + '\'' +
+ ", providerUniqueId=" + providerUniqueId +
+ ", configServerHostname='" + configServerHostname + '\'' +
+ ", instanceHostname='" + instanceHostname + '\'' +
+ ", createdAt=" + createdAt +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ IdentityDocument that = (IdentityDocument) o;
+ return Objects.equals(athensDomain, that.athensDomain) &&
+ Objects.equals(athensService, that.athensService) &&
+ Objects.equals(providerUniqueId, that.providerUniqueId) &&
+ Objects.equals(configServerHostname, that.configServerHostname) &&
+ Objects.equals(instanceHostname, that.instanceHostname) &&
+ Objects.equals(createdAt, that.createdAt);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(athensDomain, athensService, providerUniqueId, configServerHostname, instanceHostname, createdAt);
+ }
+}
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/impl/model/InstanceConfirmation.java
new file mode 100644
index 00000000000..ade42968e58
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java
@@ -0,0 +1,99 @@
+// 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;
+
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+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 java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * InstanceConfirmation object as per Athenz InstanceConfirmation API.
+ *
+ * @author bjorncs
+ */
+public class InstanceConfirmation {
+
+ @JsonProperty("provider") public final String provider;
+ @JsonProperty("domain") public final String domain;
+ @JsonProperty("service") public final String service;
+
+ @JsonProperty("attestationData") @JsonSerialize(using = SignedIdentitySerializer.class)
+ public final SignedIdentityDocument signedIdentityDocument;
+ @JsonUnwrapped public final Map<String, Object> attributes = new HashMap<>(); // optional attributes that Athenz may provide
+
+ @JsonCreator
+ public InstanceConfirmation(@JsonProperty("provider") String provider,
+ @JsonProperty("domain") String domain,
+ @JsonProperty("service") String service,
+ @JsonProperty("attestationData") @JsonDeserialize(using = SignedIdentityDeserializer.class)
+ SignedIdentityDocument signedIdentityDocument) {
+ this.provider = provider;
+ this.domain = domain;
+ this.service = service;
+ this.signedIdentityDocument = signedIdentityDocument;
+ }
+
+ @JsonAnySetter
+ public void set(String name, Object value) {
+ attributes.put(name, value);
+ }
+
+ @Override
+ public String toString() {
+ return "InstanceConfirmation{" +
+ "provider='" + provider + '\'' +
+ ", domain='" + domain + '\'' +
+ ", service='" + service + '\'' +
+ ", signedIdentityDocument='" + signedIdentityDocument + '\'' +
+ ", attributes=" + attributes +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ InstanceConfirmation that = (InstanceConfirmation) o;
+ return Objects.equals(provider, that.provider) &&
+ Objects.equals(domain, that.domain) &&
+ Objects.equals(service, that.service) &&
+ Objects.equals(signedIdentityDocument, that.signedIdentityDocument) &&
+ Objects.equals(attributes, that.attributes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(provider, domain, service, signedIdentityDocument, attributes);
+ }
+
+ public static class SignedIdentityDeserializer extends JsonDeserializer<SignedIdentityDocument> {
+ @Override
+ public SignedIdentityDocument deserialize(
+ JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
+ String value = jsonParser.getValueAsString();
+ return Utils.getMapper().readValue(value, SignedIdentityDocument.class);
+ }
+ }
+
+ public static class SignedIdentitySerializer extends JsonSerializer<SignedIdentityDocument> {
+ @Override
+ public void serialize(
+ SignedIdentityDocument document, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ gen.writeString(Utils.getMapper().writeValueAsString(document));
+ }
+ }
+}
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/impl/model/ProviderUniqueId.java
new file mode 100644
index 00000000000..4c09dd917a4
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.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.impl.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Objects;
+
+/**
+ * @author bjorncs
+ */
+public class ProviderUniqueId {
+
+ @JsonProperty("tenant") public final String tenant;
+ @JsonProperty("application") public final String application;
+ @JsonProperty("environment") public final String environment;
+ @JsonProperty("region") public final String region;
+ @JsonProperty("instance") public final String instance;
+ @JsonProperty("cluster-id") public final String clusterId;
+ @JsonProperty("cluster-index") public final int clusterIndex;
+
+ public ProviderUniqueId(@JsonProperty("tenant") String tenant,
+ @JsonProperty("application") String application,
+ @JsonProperty("environment") String environment,
+ @JsonProperty("region") String region,
+ @JsonProperty("instance") String instance,
+ @JsonProperty("cluster-id") String clusterId,
+ @JsonProperty("cluster-index") int clusterIndex) {
+ this.tenant = tenant;
+ this.application = application;
+ this.environment = environment;
+ this.region = region;
+ this.instance = instance;
+ this.clusterId = clusterId;
+ this.clusterIndex = clusterIndex;
+ }
+
+ @Override
+ public String toString() {
+ return "ProviderUniqueId{" +
+ "tenant='" + tenant + '\'' +
+ ", application='" + application + '\'' +
+ ", environment='" + environment + '\'' +
+ ", region='" + region + '\'' +
+ ", instance='" + instance + '\'' +
+ ", clusterId='" + clusterId + '\'' +
+ ", clusterIndex=" + clusterIndex +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ProviderUniqueId that = (ProviderUniqueId) o;
+ return clusterIndex == that.clusterIndex &&
+ Objects.equals(tenant, that.tenant) &&
+ Objects.equals(application, that.application) &&
+ Objects.equals(environment, that.environment) &&
+ Objects.equals(region, that.region) &&
+ Objects.equals(instance, that.instance) &&
+ Objects.equals(clusterId, that.clusterId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tenant, application, environment, region, instance, clusterId, clusterIndex);
+ }
+}
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/impl/model/SignedIdentityDocument.java
new file mode 100644
index 00000000000..df1bfe772e8
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java
@@ -0,0 +1,72 @@
+// 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;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Base64;
+import java.util.Objects;
+
+/**
+ * @author bjorncs
+ */
+public class SignedIdentityDocument {
+
+ @JsonProperty("identity-document")public final String rawIdentityDocument;
+ @JsonIgnore public final IdentityDocument identityDocument;
+ @JsonProperty("signature") public final String signature;
+ @JsonProperty("signing-key-version") public final int signingKeyVersion;
+ @JsonProperty("document-version") public final int documentVersion;
+
+ @JsonCreator
+ public SignedIdentityDocument(@JsonProperty("identity-document") String rawIdentityDocument,
+ @JsonProperty("signature") String signature,
+ @JsonProperty("signing-key-version") int signingKeyVersion,
+ @JsonProperty("document-version") int documentVersion) {
+ this.rawIdentityDocument = rawIdentityDocument;
+ this.identityDocument = parseIdentityDocument(rawIdentityDocument);
+ this.signature = signature;
+ this.signingKeyVersion = signingKeyVersion;
+ this.documentVersion = documentVersion;
+ }
+
+ private static IdentityDocument parseIdentityDocument(String rawIdentityDocument) {
+ try {
+ return Utils.getMapper().readValue(Base64.getDecoder().decode(rawIdentityDocument), IdentityDocument.class);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SignedIdentityDocument{" +
+ "rawIdentityDocument='" + rawIdentityDocument + '\'' +
+ ", identityDocument=" + identityDocument +
+ ", signature='" + signature + '\'' +
+ ", signingKeyVersion=" + signingKeyVersion +
+ ", documentVersion=" + documentVersion +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SignedIdentityDocument that = (SignedIdentityDocument) o;
+ return signingKeyVersion == that.signingKeyVersion &&
+ documentVersion == that.documentVersion &&
+ Objects.equals(rawIdentityDocument, that.rawIdentityDocument) &&
+ Objects.equals(identityDocument, that.identityDocument) &&
+ Objects.equals(signature, that.signature);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(rawIdentityDocument, identityDocument, signature, signingKeyVersion, documentVersion);
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java
new file mode 100644
index 00000000000..3024d1e0115
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author bjorncs
+ */
+@ExportPackage
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice;
+
+import com.yahoo.osgi.annotation.ExportPackage;
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
new file mode 100644
index 00000000000..3a2ef9c3092
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def
@@ -0,0 +1,29 @@
+# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.hosted.athenz.identityproviderservice.config
+
+# Athenz domain
+domain string
+
+# Athenz service name
+serviceName string
+
+# Current key version
+keyVersion int default=0
+
+# HTTPS port for Athenz Provider Service endpoint
+port int default=8443
+
+# File name prefix for private and public key. Component assumes suffix .[priv|pub].<version>.
+keyPathPrefix string
+
+# InstanceConfirmation API path
+apiPath string default="/athenz/v1/provider/instance"
+
+# Athenz principal authority header name
+athenzPrincipalHeaderName string default="Athenz-Principal-Auth"
+
+# Athenz ZTS server url
+ztsUrl string
+
+# Certificate DNS suffix
+certDnsSuffix string
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
new file mode 100644
index 00000000000..3798a1e5496
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java
@@ -0,0 +1,163 @@
+// 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.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.athenz.auth.util.Crypto;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.hosted.athenz.identityproviderservice.config.AthenzProviderServiceConfig;
+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.junit.Ignore;
+import org.junit.Test;
+
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.time.Instant;
+import java.util.Base64;
+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;
+
+/**
+ * @author bjorncs
+ */
+public class AthenzInstanceProviderServiceTest {
+
+ private static final Logger log = Logger.getLogger(AthenzInstanceProviderServiceTest.class.getName());
+ private static final int PORT = 12345;
+
+ @Test
+ @Ignore("Requires private key for Athenz service")
+ public void provider_service_hosts_endpoint_secured_with_tls() throws Exception {
+ String domain = "vespa.vespa.cd";
+ String service = "provider_dev_cd-us-central-1";
+ DummyKeyProvider keyProvider = new DummyKeyProvider();
+ PrivateKey privateKey = Crypto.loadPrivateKey(keyProvider.getPrivateKey(0));
+
+ AthenzProviderServiceConfig config =
+ new AthenzProviderServiceConfig(
+ new AthenzProviderServiceConfig.Builder()
+ .domain(domain)
+ .serviceName(service)
+ .port(PORT)
+ .keyPathPrefix("dummy-path")
+ .certDnsSuffix("INSERT DNS SUFFIX HERE")
+ .ztsUrl("INSERT ZTS URL HERE")
+ .athenzPrincipalHeaderName("INSERT PRINCIPAL HEADER NAME HERE")
+ .apiPath("/"));
+
+ ScheduledExecutorServiceMock executor = new ScheduledExecutorServiceMock();
+ AthenzInstanceProviderService athenzInstanceProviderService = new AthenzInstanceProviderService(config, keyProvider, executor);
+
+ try (CloseableHttpClient client = createHttpClient(domain, service)) {
+ Runnable certificateRefreshCommand = executor.getCommand().orElseThrow(() -> new AssertionError("Command not present"));
+ assertFalse(getStatus(client));
+ certificateRefreshCommand.run();
+ assertTrue(getStatus(client));
+ assertInstanceConfirmationSucceeds(client, privateKey);
+ certificateRefreshCommand.run();
+ assertTrue(getStatus(client));
+ assertInstanceConfirmationSucceeds(client, privateKey);
+ } finally {
+ athenzInstanceProviderService.deconstruct();
+ }
+ }
+
+ 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 + "/");
+ 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(
+ "domain", "service",
+ 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());
+ String signature = Base64.getEncoder().encodeToString(sigGenerator.sign());
+
+ InstanceConfirmation instanceConfirmation = new InstanceConfirmation(
+ "provider", "domain", "service",
+ new SignedIdentityDocument(encodedIdentityDocument, signature, 0, 1));
+ return new StringEntity(mapper.writeValueAsString(instanceConfirmation));
+ } catch (JsonProcessingException
+ | NoSuchAlgorithmException
+ | UnsupportedEncodingException
+ | SignatureException
+ | InvalidKeyException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class DummyKeyProvider implements KeyProvider {
+
+ @Override
+ public String getPrivateKey(int version) {
+ return "INSERT PRIV KEY";
+ }
+
+ @Override
+ public String getPublicKey(int version) {
+ return "INSERT PUB KEY";
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java
new file mode 100644
index 00000000000..45cb82a0c0a
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java
@@ -0,0 +1,115 @@
+// 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.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * @author bjorncs
+ */
+public class ScheduledExecutorServiceMock implements ScheduledExecutorService {
+
+ private Runnable runnable;
+
+ public Optional<Runnable> getCommand() {
+ return Optional.ofNullable(runnable);
+ }
+
+ @Override
+ public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
+ if (runnable != null) {
+ throw new IllegalStateException("Can only register single command");
+ }
+ runnable = Objects.requireNonNull(command);
+ return null;
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void shutdown() {
+ // do nothing
+ }
+
+ @Override
+ public List<Runnable> shutdownNow() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isTerminated() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+ return true;
+ }
+
+ @Override
+ public <T> Future<T> submit(Callable<T> task) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> Future<T> submit(Runnable task, T result) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Future<?> submit(Runnable task) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ throw new UnsupportedOperationException();
+ }
+}