aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHenning Baldersheim <balder@yahoo-inc.com>2023-03-04 14:27:33 +0100
committerGitHub <noreply@github.com>2023-03-04 14:27:33 +0100
commite1535b0552bd1993c31acde3606c1411cf769d5b (patch)
treed01c876617db76142fc60bc9d1fde3508dda2502
parent51600f1613c1787c3083409204452175e028cb22 (diff)
Revert "Mortent/reapply public athenz provider"
-rw-r--r--CMakeLists.txt1
-rw-r--r--athenz-identity-provider-service/CMakeLists.txt2
-rw-r--r--athenz-identity-provider-service/OWNERS1
-rw-r--r--athenz-identity-provider-service/README.md5
-rw-r--r--athenz-identity-provider-service/pom.xml186
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CertificateExpiryMetricUpdater.java59
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CkmsKeyProvider.java64
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java182
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java83
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java99
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java99
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidator.java265
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/KeyProvider.java19
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/Utils.java24
-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/java/com/yahoo/vespa/hosted/ca/Certificates.java95
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java64
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java40
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java83
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java198
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java123
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/package-info.java8
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AutoGeneratedKeyProvider.java37
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGeneratorTest.java102
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidatorTest.java296
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/TestUtils.java27
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificateTester.java79
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificatesTest.java65
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java243
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java88
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java99
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java27
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/PrincipalFromHeaderFilter.java34
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/SecretStoreMock.java34
-rw-r--r--configdefinitions/src/vespa/athenz-provider-service.def5
-rw-r--r--configserver/CMakeLists.txt1
-rw-r--r--dist/vespa.spec1
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flags.java7
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java26
-rw-r--r--pom.xml1
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java6
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java11
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java12
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSigner.java13
-rw-r--r--vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapperTest.java1
-rw-r--r--vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSignerTest.java39
-rw-r--r--vespa-dependencies-enforcer/allowed-maven-dependencies.txt2
47 files changed, 2871 insertions, 93 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c995007663d..ce11196725f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -68,6 +68,7 @@ include_directories(BEFORE ${CMAKE_BINARY_DIR}/configdefinitions/src)
add_subdirectory(airlift-zstd)
add_subdirectory(ann_benchmark)
add_subdirectory(application-model)
+add_subdirectory(athenz-identity-provider-service)
add_subdirectory(client)
add_subdirectory(cloud-tenant-cd)
add_subdirectory(clustercontroller-apps)
diff --git a/athenz-identity-provider-service/CMakeLists.txt b/athenz-identity-provider-service/CMakeLists.txt
new file mode 100644
index 00000000000..75208c49bd3
--- /dev/null
+++ b/athenz-identity-provider-service/CMakeLists.txt
@@ -0,0 +1,2 @@
+# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+install_jar(athenz-identity-provider-service-jar-with-dependencies.jar)
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/README.md b/athenz-identity-provider-service/README.md
new file mode 100644
index 00000000000..b25eb009e1b
--- /dev/null
+++ b/athenz-identity-provider-service/README.md
@@ -0,0 +1,5 @@
+<!-- Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+# Athenz Identity Provider Service
+
+An [Athenz Copper Argos](https://github.com/yahoo/athenz/blob/master/docs/copper_argos.md) provider implementation for configserver.
+
diff --git a/athenz-identity-provider-service/pom.xml b/athenz-identity-provider-service/pom.xml
new file mode 100644
index 00000000000..f4daa43b8e3
--- /dev/null
+++ b/athenz-identity-provider-service/pom.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0"?>
+<!-- Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<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>8-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <dependencies>
+ <!-- PROVIDED -->
+ <dependency>
+ <groupId>com.google.inject</groupId>
+ <artifactId>guice</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>component</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-apache-http-client-bundle</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-dev</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>jdisc_core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>node-repository</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-provisioning</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-model-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>vespa-athenz</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>security-utils</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- TEST -->
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>zkfacade</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>orchestrator</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>application</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>testutil</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-library</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>flags</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-test</artifactId>
+ <version>${project.version}</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>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <!-- Illegal reflective access by guice. TODO: try to remove for guice >3.0 -->
+ <argLine>
+ --add-opens=java.base/java.lang=ALL-UNNAMED
+ </argLine>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CertificateExpiryMetricUpdater.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CertificateExpiryMetricUpdater.java
new file mode 100644
index 00000000000..f3568caac04
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CertificateExpiryMetricUpdater.java
@@ -0,0 +1,59 @@
+// Copyright Yahoo. 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.yahoo.component.annotation.Inject;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.jdisc.Metric;
+
+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.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author freva
+ */
+public class CertificateExpiryMetricUpdater extends AbstractComponent {
+
+ private static final Duration METRIC_REFRESH_PERIOD = Duration.ofMinutes(5);
+ private static final String ATHENZ_CONFIGSERVER_CERT_METRIC_NAME = "athenz-configserver-cert.expiry.seconds";
+
+ private final Logger logger = Logger.getLogger(CertificateExpiryMetricUpdater.class.getName());
+ private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+ private final Metric metric;
+ private final ConfigserverSslContextFactoryProvider provider;
+
+ @Inject
+ public CertificateExpiryMetricUpdater(Metric metric,
+ ConfigserverSslContextFactoryProvider provider) {
+ this.metric = metric;
+ this.provider = provider;
+
+ scheduler.scheduleAtFixedRate(this::updateMetrics,
+ 30/*initial delay*/,
+ METRIC_REFRESH_PERIOD.getSeconds(),
+ TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void deconstruct() {
+ try {
+ scheduler.shutdownNow();
+ scheduler.awaitTermination(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Failed to shutdown certificate expiry metrics updater on time", e);
+ }
+ }
+
+ private void updateMetrics() {
+ try {
+ Duration keyStoreExpiry = Duration.between(Instant.now(), provider.getCertificateNotAfter());
+ metric.set(ATHENZ_CONFIGSERVER_CERT_METRIC_NAME, keyStoreExpiry.getSeconds(), null);
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Failed to update key store expiry metric: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CkmsKeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CkmsKeyProvider.java
new file mode 100644
index 00000000000..c659c454420
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CkmsKeyProvider.java
@@ -0,0 +1,64 @@
+// Copyright Yahoo. 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.yahoo.component.annotation.Inject;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.secretstore.SecretStore;
+import com.yahoo.security.KeyUtils;
+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;
+import java.security.PublicKey;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author mortent
+ * @author bjorncs
+ */
+@SuppressWarnings("unused") // Injected component
+public class CkmsKeyProvider implements KeyProvider {
+
+ private final SecretStore secretStore;
+ private final String secretName;
+ private final Map<Integer, KeyPair> secrets;
+
+ @Inject
+ public CkmsKeyProvider(SecretStore secretStore,
+ Zone zone,
+ AthenzProviderServiceConfig config) {
+ this.secretStore = secretStore;
+ this.secretName = config.secretName();
+ this.secrets = new HashMap<>();
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(int version) {
+ return getKeyPair(version).getPrivate();
+ }
+
+ @Override
+ public PublicKey getPublicKey(int version) {
+ return getKeyPair(version).getPublic();
+ }
+
+ @Override
+ public KeyPair getKeyPair(int version) {
+ synchronized (secrets) {
+ KeyPair keyPair = secrets.get(version);
+ if (keyPair == null) {
+ keyPair = readKeyPair(version);
+ secrets.put(version, keyPair);
+ }
+ return keyPair;
+ }
+ }
+
+ private KeyPair readKeyPair(int version) {
+ PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(secretStore.getSecret(secretName, version));
+ PublicKey publicKey = KeyUtils.extractPublicKey(privateKey);
+ return new KeyPair(publicKey, privateKey);
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java
new file mode 100644
index 00000000000..61a4a0fe41f
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java
@@ -0,0 +1,182 @@
+// Copyright Yahoo. 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.yahoo.component.annotation.Inject;
+import com.yahoo.jdisc.http.ssl.impl.TlsContextBasedProvider;
+import com.yahoo.security.KeyStoreBuilder;
+import com.yahoo.security.KeyStoreType;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SslContextBuilder;
+import com.yahoo.security.tls.DefaultTlsContext;
+import com.yahoo.security.MutableX509KeyManager;
+import com.yahoo.security.tls.PeerAuthentication;
+import com.yahoo.security.tls.TlsContext;
+import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient;
+import com.yahoo.vespa.athenz.client.zts.Identity;
+import com.yahoo.vespa.athenz.client.zts.ZtsClient;
+import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
+import com.yahoo.vespa.athenz.utils.SiaUtils;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
+
+import javax.net.ssl.SSLContext;
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Configures the JDisc https connector with the configserver's Athenz provider certificate and private key.
+ *
+ * @author bjorncs
+ */
+public class ConfigserverSslContextFactoryProvider extends TlsContextBasedProvider {
+
+ private static final String CERTIFICATE_ALIAS = "athenz";
+ private static final Duration EXPIRATION_MARGIN = Duration.ofHours(6);
+ private static final Path VESPA_SIA_DIRECTORY = Paths.get(Defaults.getDefaults().underVespaHome("var/vespa/sia"));
+
+ private static final Logger log = Logger.getLogger(ConfigserverSslContextFactoryProvider.class.getName());
+
+ private final TlsContext tlsContext;
+ private final MutableX509KeyManager keyManager = new MutableX509KeyManager();
+ private final ScheduledExecutorService scheduler =
+ Executors.newSingleThreadScheduledExecutor(runnable -> new Thread(runnable, "configserver-ssl-context-factory-provider"));
+ private final ZtsClient ztsClient;
+ private final KeyProvider keyProvider;
+ private final AthenzProviderServiceConfig athenzProviderServiceConfig;
+ private final AthenzService configserverIdentity;
+
+ @Inject
+ public ConfigserverSslContextFactoryProvider(ServiceIdentityProvider bootstrapIdentity,
+ KeyProvider keyProvider,
+ AthenzProviderServiceConfig config) {
+ this.athenzProviderServiceConfig = config;
+ this.ztsClient = new DefaultZtsClient.Builder(URI.create(athenzProviderServiceConfig.ztsUrl()))
+ .withIdentityProvider(bootstrapIdentity).build();
+ this.keyProvider = keyProvider;
+ this.configserverIdentity = new AthenzService(athenzProviderServiceConfig.domain(), athenzProviderServiceConfig.serviceName());
+
+ Duration updatePeriod = Duration.ofDays(config.updatePeriodDays());
+ Path trustStoreFile = Paths.get(config.athenzCaTrustStore());
+ this.tlsContext = createTlsContext(keyProvider, keyManager, trustStoreFile, updatePeriod, configserverIdentity, ztsClient, athenzProviderServiceConfig);
+ scheduler.scheduleAtFixedRate(new KeystoreUpdater(keyManager),
+ updatePeriod.toDays()/*initial delay*/,
+ updatePeriod.toDays(),
+ TimeUnit.DAYS);
+ }
+
+ @Override
+ protected TlsContext getTlsContext(String containerId, int port) {
+ return tlsContext;
+ }
+
+ Instant getCertificateNotAfter() {
+ return keyManager.currentManager().getCertificateChain(CERTIFICATE_ALIAS)[0].getNotAfter().toInstant();
+ }
+
+ @Override
+ public void deconstruct() {
+ try {
+ scheduler.shutdownNow();
+ scheduler.awaitTermination(30, TimeUnit.SECONDS);
+ ztsClient.close();
+ super.deconstruct();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Failed to shutdown Athenz certificate updater on time", e);
+ }
+ }
+
+ private static TlsContext createTlsContext(KeyProvider keyProvider,
+ MutableX509KeyManager keyManager,
+ Path trustStoreFile,
+ Duration updatePeriod,
+ AthenzService configserverIdentity,
+ ZtsClient ztsClient,
+ AthenzProviderServiceConfig zoneConfig) {
+ KeyStore keyStore =
+ tryReadKeystoreFile(configserverIdentity, updatePeriod)
+ .orElseGet(() -> updateKeystore(configserverIdentity, generateKeystorePassword(), keyProvider, ztsClient, zoneConfig));
+ keyManager.updateKeystore(keyStore, new char[0]);
+ SSLContext sslContext = new SslContextBuilder()
+ .withTrustStore(trustStoreFile, KeyStoreType.JKS)
+ .withKeyManager(keyManager)
+ .build();
+ return new DefaultTlsContext(sslContext, PeerAuthentication.WANT);
+ }
+
+ private static Optional<KeyStore> tryReadKeystoreFile(AthenzService configserverIdentity, Duration updatePeriod) {
+ Optional<X509Certificate> certificate = SiaUtils.readCertificateFile(VESPA_SIA_DIRECTORY, configserverIdentity);
+ if (!certificate.isPresent()) return Optional.empty();
+ Optional<PrivateKey> privateKey = SiaUtils.readPrivateKeyFile(VESPA_SIA_DIRECTORY, configserverIdentity);
+ if (!privateKey.isPresent()) return Optional.empty();
+ Instant minimumExpiration = Instant.now().plus(updatePeriod).plus(EXPIRATION_MARGIN);
+ boolean isExpired = certificate.get().getNotAfter().toInstant().isBefore(minimumExpiration);
+ if (isExpired) return Optional.empty();
+ KeyStore keyStore = KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .withKeyEntry(CERTIFICATE_ALIAS, privateKey.get(), certificate.get())
+ .build();
+ return Optional.of(keyStore);
+ }
+
+ private static KeyStore updateKeystore(AthenzService configserverIdentity,
+ char[] keystorePwd,
+ KeyProvider keyProvider,
+ ZtsClient ztsClient,
+ AthenzProviderServiceConfig zoneConfig) {
+ PrivateKey privateKey = keyProvider.getPrivateKey(zoneConfig.secretVersion());
+ PublicKey publicKey = KeyUtils.extractPublicKey(privateKey);
+ Identity serviceIdentity = ztsClient.getServiceIdentity(configserverIdentity,
+ Integer.toString(zoneConfig.secretVersion()),
+ new KeyPair(publicKey, privateKey),
+ zoneConfig.certDnsSuffix());
+ X509Certificate certificate = serviceIdentity.certificate();
+ SiaUtils.writeCertificateFile(VESPA_SIA_DIRECTORY, configserverIdentity, certificate);
+ SiaUtils.writePrivateKeyFile(VESPA_SIA_DIRECTORY, configserverIdentity, privateKey);
+ Instant expirationTime = certificate.getNotAfter().toInstant();
+ Duration expiry = Duration.between(certificate.getNotBefore().toInstant(), expirationTime);
+ log.log(Level.INFO, String.format("Got Athenz x509 certificate with expiry %s (expires %s)", expiry, expirationTime));
+ return KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .withKeyEntry(CERTIFICATE_ALIAS, privateKey, keystorePwd, certificate)
+ .build();
+ }
+
+ private static char[] generateKeystorePassword() {
+ return UUID.randomUUID().toString().toCharArray();
+ }
+
+ private class KeystoreUpdater implements Runnable {
+ final MutableX509KeyManager keyManager;
+
+ KeystoreUpdater(MutableX509KeyManager keyManager) {
+ this.keyManager = keyManager;
+ }
+
+ @Override
+ public void run() {
+ try {
+ log.log(Level.INFO, "Updating configserver provider certificate from ZTS");
+ char[] keystorePwd = generateKeystorePassword();
+ KeyStore keyStore = updateKeystore(configserverIdentity, keystorePwd, keyProvider, ztsClient, athenzProviderServiceConfig);
+ keyManager.updateKeystore(keyStore, keystorePwd);
+ log.log(Level.INFO, "Certificate successfully updated");
+ } catch (Throwable t) {
+ log.log(Level.SEVERE, "Failed to update certificate from ZTS: " + t.getMessage(), t);
+ }
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java
new file mode 100644
index 00000000000..5143a38b2c1
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java
@@ -0,0 +1,83 @@
+// Copyright Yahoo. 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.yahoo.component.annotation.Inject;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.net.HostName;
+import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.identityprovider.api.ClusterType;
+import com.yahoo.vespa.athenz.identityprovider.api.IdentityType;
+import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
+import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
+import com.yahoo.vespa.athenz.identityprovider.client.IdentityDocumentSigner;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
+import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeRepository;
+import com.yahoo.vespa.hosted.provision.node.Allocation;
+
+import java.security.PrivateKey;
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Generates a signed identity document for a given hostname and type
+ *
+ * @author mortent
+ * @author bjorncs
+ */
+public class IdentityDocumentGenerator {
+
+ private final IdentityDocumentSigner signer = new IdentityDocumentSigner();
+ private final NodeRepository nodeRepository;
+ private final Zone zone;
+ private final KeyProvider keyProvider;
+ private final AthenzProviderServiceConfig athenzProviderServiceConfig;
+
+ @Inject
+ public IdentityDocumentGenerator(AthenzProviderServiceConfig config,
+ NodeRepository nodeRepository,
+ Zone zone,
+ KeyProvider keyProvider) {
+ this.athenzProviderServiceConfig = config;
+ this.nodeRepository = nodeRepository;
+ this.zone = zone;
+ this.keyProvider = keyProvider;
+ }
+
+ public SignedIdentityDocument generateSignedIdentityDocument(String hostname, IdentityType identityType) {
+ try {
+ Node node = nodeRepository.nodes().node(hostname).orElseThrow(() -> new RuntimeException("Unable to find node " + hostname));
+ Allocation allocation = node.allocation().orElseThrow(() -> new RuntimeException("No allocation for node " + node.hostname()));
+ VespaUniqueInstanceId providerUniqueId = new VespaUniqueInstanceId(
+ allocation.membership().index(),
+ allocation.membership().cluster().id().value(),
+ allocation.owner().instance().value(),
+ allocation.owner().application().value(),
+ allocation.owner().tenant().value(),
+ zone.region().value(),
+ zone.environment().value(),
+ identityType);
+
+ Set<String> ips = new HashSet<>(node.ipConfig().primary());
+
+ PrivateKey privateKey = keyProvider.getPrivateKey(athenzProviderServiceConfig.secretVersion());
+ AthenzService providerService = new AthenzService(athenzProviderServiceConfig.domain(), athenzProviderServiceConfig.serviceName());
+
+ String configServerHostname = HostName.getLocalhost();
+ Instant createdAt = Instant.now();
+ var clusterType = ClusterType.from(allocation.membership().cluster().type().name());
+ String signature = signer.generateSignature(
+ providerUniqueId, providerService, configServerHostname,
+ node.hostname(), createdAt, ips, identityType, privateKey);
+ return new SignedIdentityDocument(
+ signature, athenzProviderServiceConfig.secretVersion(), providerUniqueId, providerService,
+ SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION, configServerHostname, node.hostname(),
+ createdAt, ips, identityType, clusterType);
+ } 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/IdentityProviderRequestHandler.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java
new file mode 100644
index 00000000000..c1dd70d7656
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java
@@ -0,0 +1,99 @@
+// Copyright Yahoo. 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.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.yahoo.component.annotation.Inject;
+import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
+import com.yahoo.restapi.RestApi;
+import com.yahoo.restapi.RestApiException;
+import com.yahoo.restapi.RestApiRequestHandler;
+import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper;
+import com.yahoo.vespa.athenz.identityprovider.api.IdentityType;
+import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity;
+
+import java.util.logging.Level;
+
+/**
+ * Handler implementing the Athenz Identity Provider API (Copper Argos).
+ *
+ * @author bjorncs
+ */
+public class IdentityProviderRequestHandler extends RestApiRequestHandler<IdentityProviderRequestHandler> {
+
+ private final IdentityDocumentGenerator documentGenerator;
+ private final InstanceValidator instanceValidator;
+
+ @Inject
+ public IdentityProviderRequestHandler(ThreadedHttpRequestHandler.Context context,
+ IdentityDocumentGenerator documentGenerator,
+ InstanceValidator instanceValidator) {
+ super(context, IdentityProviderRequestHandler::createRestApi);
+ this.documentGenerator = documentGenerator;
+ this.instanceValidator = instanceValidator;
+ }
+
+ private static RestApi createRestApi(IdentityProviderRequestHandler self) {
+ return RestApi.builder()
+ .addRoute(RestApi.route("/athenz/v1/provider/identity-document/node/{host}")
+ .get(self::getNodeIdentityDocument))
+ .addRoute(RestApi.route("/athenz/v1/provider/identity-document/tenant/{host}")
+ .get(self::getTenantIdentityDocument))
+ .addRoute(RestApi.route("/athenz/v1/provider/instance")
+ .post(InstanceConfirmation.class, self::confirmInstance))
+ .addRoute(RestApi.route("/athenz/v1/provider/refresh")
+ .post(InstanceConfirmation.class, self::confirmInstanceRefresh))
+ .registerJacksonRequestEntity(InstanceConfirmation.class)
+ .registerJacksonResponseEntity(InstanceConfirmation.class)
+ .registerJacksonResponseEntity(SignedIdentityDocumentEntity.class)
+ // Overriding object mapper to change serialization of timestamps
+ .setObjectMapper(new ObjectMapper()
+ .registerModule(new JavaTimeModule())
+ .registerModule(new Jdk8Module())
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true))
+ .build();
+ }
+
+ private SignedIdentityDocumentEntity getNodeIdentityDocument(RestApi.RequestContext context) {
+ String host = context.pathParameters().getString("host").orElse(null);
+ return getIdentityDocument(host, IdentityType.NODE);
+ }
+
+ private SignedIdentityDocumentEntity getTenantIdentityDocument(RestApi.RequestContext context) {
+ String host = context.pathParameters().getString("host").orElse(null);
+ return getIdentityDocument(host, IdentityType.TENANT);
+ }
+
+ private InstanceConfirmation confirmInstance(RestApi.RequestContext context, InstanceConfirmation instanceConfirmation) {
+ log.log(Level.FINE, () -> instanceConfirmation.toString());
+ if (!instanceValidator.isValidInstance(instanceConfirmation)) {
+ log.log(Level.SEVERE, "Invalid instance: " + instanceConfirmation);
+ throw new RestApiException.Forbidden("Instance is invalid");
+ }
+ return instanceConfirmation;
+ }
+
+ private InstanceConfirmation confirmInstanceRefresh(RestApi.RequestContext context, InstanceConfirmation instanceConfirmation) {
+ log.log(Level.FINE, () -> instanceConfirmation.toString());
+ if (!instanceValidator.isValidRefresh(instanceConfirmation)) {
+ log.log(Level.SEVERE, "Invalid instance refresh: " + instanceConfirmation);
+ throw new RestApiException.Forbidden("Instance is invalid");
+ }
+ return instanceConfirmation;
+ }
+
+ private SignedIdentityDocumentEntity getIdentityDocument(String hostname, IdentityType identityType) {
+ if (hostname == null) {
+ throw new RestApiException.BadRequest("The 'hostname' query parameter is missing");
+ }
+ try {
+ return EntityBindingsMapper.toSignedIdentityDocumentEntity(documentGenerator.generateSignedIdentityDocument(hostname, identityType));
+ } catch (Exception e) {
+ String message = String.format("Unable to generate identity document for '%s': %s", hostname, e.getMessage());
+ log.log(Level.SEVERE, message, e);
+ throw new RestApiException.InternalServerError(message, e);
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java
new file mode 100644
index 00000000000..6c09a35ee3d
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java
@@ -0,0 +1,99 @@
+// Copyright Yahoo. 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.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.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity;
+
+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 SignedIdentityDocumentEntity signedIdentityDocument;
+ @JsonUnwrapped public final Map<String, String> 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)
+ SignedIdentityDocumentEntity signedIdentityDocument) {
+ this.provider = provider;
+ this.domain = domain;
+ this.service = service;
+ this.signedIdentityDocument = signedIdentityDocument;
+ }
+
+ @JsonAnySetter
+ public void set(String name, String 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<SignedIdentityDocumentEntity> {
+ @Override
+ public SignedIdentityDocumentEntity deserialize(
+ JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
+ String value = jsonParser.getValueAsString();
+ return Utils.getMapper().readValue(value, SignedIdentityDocumentEntity.class);
+ }
+ }
+
+ public static class SignedIdentitySerializer extends JsonSerializer<SignedIdentityDocumentEntity> {
+ @Override
+ public void serialize(
+ SignedIdentityDocumentEntity 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/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidator.java
new file mode 100644
index 00000000000..d8bbf743d8c
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidator.java
@@ -0,0 +1,265 @@
+// Copyright Yahoo. 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.net.InetAddresses;
+import com.yahoo.component.annotation.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.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.identityprovider.api.ClusterType;
+import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper;
+import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
+import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
+import com.yahoo.vespa.athenz.identityprovider.client.IdentityDocumentSigner;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
+import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeRepository;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * Verifies that the instance's identity document is valid
+ *
+ * @author bjorncs
+ * @author mortent
+ */
+public class InstanceValidator {
+
+ private static final Logger log = Logger.getLogger(InstanceValidator.class.getName());
+ static final String SERVICE_PROPERTIES_DOMAIN_KEY = "identity.domain";
+ static final String SERVICE_PROPERTIES_SERVICE_KEY = "identity.service";
+ static final String INSTANCE_ID_DELIMITER = ".instanceid.athenz.";
+
+ public static final String SAN_IPS_ATTRNAME = "sanIP";
+ public static final String SAN_DNS_ATTRNAME = "sanDNS";
+ public static final String SAN_URI_ATTRNAME = "sanURI";
+
+ private final AthenzService tenantDockerContainerIdentity;
+ private final IdentityDocumentSigner signer;
+ private final KeyProvider keyProvider;
+ private final SuperModelProvider superModelProvider;
+ private final NodeRepository nodeRepository;
+
+ @Inject
+ public InstanceValidator(KeyProvider keyProvider,
+ SuperModelProvider superModelProvider,
+ NodeRepository nodeRepository,
+ AthenzProviderServiceConfig config) {
+ this(keyProvider, superModelProvider, nodeRepository, new IdentityDocumentSigner(), new AthenzService(config.tenantService()));
+ }
+
+ public InstanceValidator(KeyProvider keyProvider,
+ SuperModelProvider superModelProvider,
+ NodeRepository nodeRepository,
+ IdentityDocumentSigner identityDocumentSigner,
+ AthenzService tenantIdentity){
+ this.keyProvider = keyProvider;
+ this.superModelProvider = superModelProvider;
+ this.nodeRepository = nodeRepository;
+ this.signer = identityDocumentSigner;
+ this.tenantDockerContainerIdentity = tenantIdentity;
+ }
+
+ public boolean isValidInstance(InstanceConfirmation instanceConfirmation) {
+ try {
+ validateInstance(instanceConfirmation);
+ return true;
+ } catch (ValidationException e) {
+ log.log(e.logLevel(), e.messageSupplier());
+ return false;
+ }
+ }
+
+ public void validateInstance(InstanceConfirmation req) throws ValidationException {
+ SignedIdentityDocument signedIdentityDocument = EntityBindingsMapper.toSignedIdentityDocument(req.signedIdentityDocument);
+ VespaUniqueInstanceId providerUniqueId = signedIdentityDocument.providerUniqueId();
+ ApplicationId applicationId = ApplicationId.from(
+ providerUniqueId.tenant(), providerUniqueId.application(), providerUniqueId.instance());
+
+ VespaUniqueInstanceId csrProviderUniqueId = getVespaUniqueInstanceId(req);
+ if(! providerUniqueId.equals(csrProviderUniqueId)) {
+ var msg = String.format("Instance %s has invalid provider unique ID in CSR (%s)", providerUniqueId, csrProviderUniqueId);
+ throw new ValidationException(Level.WARNING, () -> msg);
+ }
+
+ if (! isSameIdentityAsInServicesXml(applicationId, req.domain, req.service)) {
+ Supplier<String> msg = () -> "Invalid identity '%s.%s' in services.xml".formatted(req.domain, req.service);
+ throw new ValidationException(Level.FINE, msg);
+ }
+
+ log.log(Level.FINE, () -> String.format("Validating instance %s.", providerUniqueId));
+
+ PublicKey publicKey = keyProvider.getPublicKey(signedIdentityDocument.signingKeyVersion());
+ if (! signer.hasValidSignature(signedIdentityDocument, publicKey)) {
+ var msg = String.format("Instance %s has invalid signature.", providerUniqueId);
+ throw new ValidationException(Level.SEVERE, () -> msg);
+ }
+
+ validateAttributes(req, providerUniqueId);
+ log.log(Level.FINE, () -> String.format("Instance %s is valid.", providerUniqueId));
+ }
+
+ // TODO Add actual validation. Cannot reuse isValidInstance as identity document is not part of the refresh request.
+ // We'll have to perform some validation on the instance id and other fields of the attribute map.
+ // Separate between tenant and node certificate as well.
+ public boolean isValidRefresh(InstanceConfirmation confirmation) {
+ log.log(Level.FINE, () -> String.format("Accepting refresh for instance with identity '%s', provider '%s', instanceId '%s'.",
+ new AthenzService(confirmation.domain, confirmation.service).getFullName(),
+ confirmation.provider,
+ confirmation.attributes.get(SAN_DNS_ATTRNAME)));
+ try {
+ validateAttributes(confirmation, getVespaUniqueInstanceId(confirmation));
+ return true;
+ } catch (ValidationException e) {
+ log.log(e.logLevel(), e.messageSupplier());
+ return false;
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Encountered exception while refreshing certificate for confirmation: " + confirmation, e);
+ return false;
+ }
+ }
+
+ private VespaUniqueInstanceId getVespaUniqueInstanceId(InstanceConfirmation instanceConfirmation) {
+ // Find a list of SAN DNS
+ List<String> sanDNS = Optional.ofNullable(instanceConfirmation.attributes.get(SAN_DNS_ATTRNAME))
+ .map(s -> s.split(","))
+ .map(Arrays::asList).stream().flatMap(Collection::stream).toList();
+
+ return sanDNS.stream()
+ .filter(dns -> dns.contains(INSTANCE_ID_DELIMITER))
+ .findFirst()
+ .map(s -> s.replaceAll(INSTANCE_ID_DELIMITER + ".*", ""))
+ .map(VespaUniqueInstanceId::fromDottedString)
+ .orElse(null);
+ }
+
+ private void validateAttributes(InstanceConfirmation confirmation, VespaUniqueInstanceId vespaUniqueInstanceId)
+ throws ValidationException {
+ if(vespaUniqueInstanceId == null) {
+ var msg = "Unable to find unique instance ID in refresh request: " + confirmation.toString();
+ throw new ValidationException(Level.WARNING, () -> msg);
+ }
+
+ // Find node matching vespa unique id
+ Node node = nodeRepository.nodes().list().stream()
+ .filter(n -> n.allocation().isPresent())
+ .filter(n -> nodeMatchesVespaUniqueId(n, vespaUniqueInstanceId))
+ .findFirst() // Should be only one
+ .orElse(null);
+ if(node == null) {
+ var msg = "Invalid InstanceConfirmation, No nodes matching uniqueId: " + vespaUniqueInstanceId;
+ throw new ValidationException(Level.WARNING, () -> msg);
+ }
+
+ // Find list of ipaddresses
+ List<InetAddress> ips = Optional.ofNullable(confirmation.attributes.get(SAN_IPS_ATTRNAME))
+ .map(s -> s.split(","))
+ .map(Arrays::asList).stream().flatMap(Collection::stream)
+ .map(InetAddresses::forString)
+ .toList();
+
+ List<InetAddress> nodeIpAddresses = node.ipConfig().primary().stream()
+ .map(InetAddresses::forString)
+ .toList();
+
+ // Validate that ipaddresses in request are valid for node
+
+ if(! nodeIpAddresses.containsAll(ips)) {
+ var msg = "Invalid InstanceConfirmation, wrong ip in : " + vespaUniqueInstanceId;
+ throw new ValidationException(Level.WARNING, () -> msg);
+ }
+
+ var urisCommaSeparated = confirmation.attributes.get(SAN_URI_ATTRNAME);
+ Set<URI> requestedUris;
+ try {
+ requestedUris = Optional.ofNullable(urisCommaSeparated).stream()
+ .flatMap(s -> Arrays.stream(s.split(","))).map(URI::create).collect(Collectors.toSet());
+ } catch (IllegalArgumentException e) {
+ throw new ValidationException(Level.WARNING, () -> "Invalid SAN URIs: " + urisCommaSeparated, e);
+ }
+ var clusterType = node.allocation().map(a -> a.membership().cluster().type()).orElse(null);
+ Set<URI> allowedUris = clusterType != null
+ ? Set.of(ClusterType.from(clusterType.name()).asCertificateSanUri()) : Set.of();
+ if (!allowedUris.containsAll(requestedUris)) {
+ Supplier<String> msg = () -> "Illegal SAN URIs: expected '%s' found '%s'".formatted(allowedUris, requestedUris);
+ throw new ValidationException(Level.WARNING, msg);
+ }
+ }
+
+ private boolean nodeMatchesVespaUniqueId(Node node, VespaUniqueInstanceId vespaUniqueInstanceId) {
+ return node.allocation().map(allocation ->
+ allocation.membership().index() == vespaUniqueInstanceId.clusterIndex() &&
+ allocation.membership().cluster().id().value().equals(vespaUniqueInstanceId.clusterId()) &&
+ allocation.owner().instance().value().equals(vespaUniqueInstanceId.instance()) &&
+ allocation.owner().application().value().equals(vespaUniqueInstanceId.application()) &&
+ allocation.owner().tenant().value().equals(vespaUniqueInstanceId.tenant()))
+ .orElse(false);
+ }
+
+ // If/when we don't care about logging exactly whats wrong, this can be simplified
+ // TODO Use identity type to determine if this check should be performed
+ private boolean isSameIdentityAsInServicesXml(ApplicationId applicationId, String domain, String service) {
+
+ Optional<ApplicationInfo> applicationInfo = superModelProvider.getSuperModel().getApplicationInfo(applicationId);
+
+ if (applicationInfo.isEmpty()) {
+ log.info(String.format("Could not find application info for %s, existing applications: %s",
+ applicationId.serializedForm(),
+ superModelProvider.getSuperModel().getAllApplicationInfos()));
+ return false;
+ }
+
+ if (tenantDockerContainerIdentity.equals(new AthenzService(domain, service))) {
+ return true;
+ }
+
+ Optional<ServiceInfo> matchingServiceInfo = applicationInfo.get()
+ .getModel()
+ .getHosts()
+ .stream()
+ .flatMap(hostInfo -> hostInfo.getServices().stream())
+ .filter(serviceInfo -> serviceInfo.getProperty(SERVICE_PROPERTIES_DOMAIN_KEY).isPresent())
+ .filter(serviceInfo -> serviceInfo.getProperty(SERVICE_PROPERTIES_SERVICE_KEY).isPresent())
+ .findFirst();
+
+ if (matchingServiceInfo.isEmpty()) {
+ log.info(String.format("Application %s has not specified domain/service", applicationId.serializedForm()));
+ return false;
+ }
+
+ String domainInConfig = matchingServiceInfo.get().getProperty(SERVICE_PROPERTIES_DOMAIN_KEY).get();
+ String serviceInConfig = matchingServiceInfo.get().getProperty(SERVICE_PROPERTIES_SERVICE_KEY).get();
+ if (!domainInConfig.equals(domain) || !serviceInConfig.equals(service)) {
+ log.warning(String.format("domain '%s' or service '%s' does not match the one in config for application %s",
+ domain, service, applicationId.serializedForm()));
+ return false;
+ }
+
+ return true;
+ }
+
+ public static class ValidationException extends Exception {
+ private final Level logLevel;
+ private final Supplier<String> msg;
+
+ public ValidationException(Level logLevel, Supplier<String> msg) { this(logLevel, msg, null); }
+ public ValidationException(Level logLevel, Supplier<String> msg, Throwable cause) { super(cause); this.logLevel = logLevel; this.msg = msg; }
+
+ @Override public String getMessage() { return msg.get(); }
+ public Level logLevel() { return logLevel; }
+ public Supplier<String> messageSupplier() { return msg; }
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/KeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/KeyProvider.java
new file mode 100644
index 00000000000..324f927fd73
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/KeyProvider.java
@@ -0,0 +1,19 @@
+// Copyright Yahoo. 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.PrivateKey;
+import java.security.PublicKey;
+
+/**
+ * @author bjorncs
+ */
+public interface KeyProvider {
+ PrivateKey getPrivateKey(int version);
+
+ PublicKey getPublicKey(int version);
+
+ default KeyPair getKeyPair(int version) {
+ return new KeyPair(getPublicKey(version), getPrivateKey(version));
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/Utils.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/Utils.java
new file mode 100644
index 00000000000..5c4942f37cb
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/Utils.java
@@ -0,0 +1,24 @@
+// Copyright Yahoo. 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.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/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..0cb5c9d4f82
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java
@@ -0,0 +1,8 @@
+// Copyright Yahoo. 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/java/com/yahoo/vespa/hosted/ca/Certificates.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java
new file mode 100644
index 00000000000..df904bf8010
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java
@@ -0,0 +1,95 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca;
+
+import com.yahoo.security.Pkcs10Csr;
+import com.yahoo.security.SubjectAlternativeName;
+import com.yahoo.security.X509CertificateBuilder;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
+
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA;
+import static com.yahoo.security.SubjectAlternativeName.Type.DNS;
+
+/**
+ * Helper class for creating {@link X509Certificate}s.
+ *
+ * @author mpolden
+ */
+public class Certificates {
+
+ private static final Duration CERTIFICATE_TTL = Duration.ofDays(30);
+ private static final String INSTANCE_ID_DELIMITER = ".instanceid.athenz.";
+
+ private final Clock clock;
+
+ public Certificates(Clock clock) {
+ this.clock = Objects.requireNonNull(clock, "clock must be non-null");
+ }
+
+ /** Create a new certificate from csr signed by the given CA private key */
+ public X509Certificate create(Pkcs10Csr csr, X509Certificate caCertificate, PrivateKey caPrivateKey) {
+ var x500principal = caCertificate.getSubjectX500Principal();
+ var now = clock.instant();
+ var notBefore = now.minus(Duration.ofHours(1));
+ var notAfter = now.plus(CERTIFICATE_TTL);
+ var builder = X509CertificateBuilder.fromCsr(csr,
+ x500principal,
+ notBefore,
+ notAfter,
+ caPrivateKey,
+ SHA256_WITH_ECDSA,
+ X509CertificateBuilder.generateRandomSerialNumber());
+ for (var san : csr.getSubjectAlternativeNames()) {
+ builder = builder.addSubjectAlternativeName(san.decode());
+ }
+ return builder.build();
+ }
+
+ /** Returns instance ID parsed from the Subject Alternative Names in given csr */
+ public static String instanceIdFrom(Pkcs10Csr csr) {
+ return getInstanceIdFromSAN(csr.getSubjectAlternativeNames())
+ .orElseThrow(() -> new IllegalArgumentException("No instance ID found in CSR"));
+ }
+
+ public static Optional<String> instanceIdFrom(X509Certificate certificate) {
+ return getInstanceIdFromSAN(X509CertificateUtils.getSubjectAlternativeNames(certificate));
+ }
+
+ private static Optional<String> getInstanceIdFromSAN(List<SubjectAlternativeName> subjectAlternativeNames) {
+ return subjectAlternativeNames.stream()
+ .filter(san -> san.getType() == DNS)
+ .map(SubjectAlternativeName::getValue)
+ .map(Certificates::parseInstanceId)
+ .flatMap(Optional::stream)
+ .map(VespaUniqueInstanceId::asDottedString)
+ .findFirst();
+ }
+
+ private static Optional<VespaUniqueInstanceId> parseInstanceId(String dnsName) {
+ var delimiterStart = dnsName.indexOf(INSTANCE_ID_DELIMITER);
+ if (delimiterStart == -1) return Optional.empty();
+ dnsName = dnsName.substring(0, delimiterStart);
+ try {
+ return Optional.of(VespaUniqueInstanceId.fromDottedString(dnsName));
+ } catch (IllegalArgumentException e) {
+ return Optional.empty();
+ }
+ }
+
+ public static String getSubjectAlternativeNames(Pkcs10Csr csr, SubjectAlternativeName.Type sanType) {
+ return csr.getSubjectAlternativeNames().stream()
+ .map(SubjectAlternativeName::decode)
+ .filter(san -> san.getType() == sanType)
+ .map(SubjectAlternativeName::getValue)
+ .collect(Collectors.joining(","));
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java
new file mode 100644
index 00000000000..f33ec4fbd6d
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java
@@ -0,0 +1,64 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.instance;
+
+import java.security.cert.X509Certificate;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * A signed instance identity object that includes a client certificate. This is the result of a successful
+ * {@link InstanceRegistration} and is the same type as InstanceIdentity in the ZTS API.
+ *
+ * @author mpolden
+ */
+public class InstanceIdentity {
+
+ private final String provider;
+ private final String service;
+ private final String instanceId;
+ private final Optional<X509Certificate> x509Certificate;
+
+ public InstanceIdentity(String provider, String service, String instanceId, Optional<X509Certificate> x509Certificate) {
+ this.provider = Objects.requireNonNull(provider, "provider must be non-null");
+ this.service = Objects.requireNonNull(service, "service must be non-null");
+ this.instanceId = Objects.requireNonNull(instanceId, "instanceId must be non-null");
+ this.x509Certificate = Objects.requireNonNull(x509Certificate, "x509Certificate must be non-null");
+ }
+
+ /** Same as {@link InstanceRegistration#domain()} */
+ public String provider() {
+ return provider;
+ }
+
+ /** Same as {@link InstanceRegistration#service()} ()} */
+ public String service() {
+ return service;
+ }
+
+ /** A unique identifier of the instance to which the certificate is issued */
+ public String instanceId() {
+ return instanceId;
+ }
+
+ /** The issued certificate */
+ public Optional<X509Certificate> x509Certificate() {
+ return x509Certificate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ InstanceIdentity that = (InstanceIdentity) o;
+ return provider.equals(that.provider) &&
+ service.equals(that.service) &&
+ instanceId.equals(that.instanceId) &&
+ x509Certificate.equals(that.x509Certificate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(provider, service, instanceId, x509Certificate);
+ }
+
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java
new file mode 100644
index 00000000000..d63ee7f979f
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java
@@ -0,0 +1,40 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.instance;
+
+import com.yahoo.security.Pkcs10Csr;
+
+import java.util.Objects;
+
+/**
+ * Information for refreshing a instance in the system. This is the same type as InstanceRefreshInformation type in
+ * the ZTS API.
+ *
+ * @author mpolden
+ */
+public class InstanceRefresh {
+
+ private final Pkcs10Csr csr;
+
+ public InstanceRefresh(Pkcs10Csr csr) {
+ this.csr = Objects.requireNonNull(csr, "csr must be non-null");
+ }
+
+ /** The Certificate Signed Request describing the wanted certificate */
+ public Pkcs10Csr csr() {
+ return csr;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ InstanceRefresh that = (InstanceRefresh) o;
+ return csr.equals(that.csr);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(csr);
+ }
+
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java
new file mode 100644
index 00000000000..231954976bf
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java
@@ -0,0 +1,83 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.instance;
+
+import com.yahoo.security.Pkcs10Csr;
+import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
+
+import java.util.Objects;
+
+/**
+ * Information for registering a new instance in the system. This is the same type as InstanceRegisterInformation type
+ * in the ZTS API.
+ *
+ * @author mpolden
+ */
+public class InstanceRegistration {
+
+ private final String provider;
+ private final String domain;
+ private final String service;
+ private final SignedIdentityDocument attestationData;
+ private final Pkcs10Csr csr;
+
+ public InstanceRegistration(String provider, String domain, String service, SignedIdentityDocument attestationData, Pkcs10Csr csr) {
+ this.provider = Objects.requireNonNull(provider, "provider must be non-null");
+ this.domain = Objects.requireNonNull(domain, "domain must be non-null");
+ this.service = Objects.requireNonNull(service, "service must be non-null");
+ this.attestationData = Objects.requireNonNull(attestationData, "attestationData must be non-null");
+ this.csr = Objects.requireNonNull(csr, "csr must be non-null");
+ }
+
+ /** The provider which issued the attestation data contained in this */
+ public String provider() {
+ return provider;
+ }
+
+ /** Athenz domain of the instance */
+ public String domain() {
+ return domain;
+ }
+
+ /** Athenz service of the instance */
+ public String service() {
+ return service;
+ }
+
+ /** Host document describing this instance (received from config server) */
+ public SignedIdentityDocument attestationData() {
+ return attestationData;
+ }
+
+ /** The Certificate Signed Request describing the wanted certificate */
+ public Pkcs10Csr csr() {
+ return csr;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ InstanceRegistration that = (InstanceRegistration) o;
+ return provider.equals(that.provider) &&
+ domain.equals(that.domain) &&
+ service.equals(that.service) &&
+ attestationData.equals(that.attestationData) &&
+ csr.equals(that.csr);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(provider, domain, service, attestationData, csr);
+ }
+
+ @Override
+ public String toString() {
+ return "InstanceRegistration{" +
+ "provider='" + provider + '\'' +
+ ", domain='" + domain + '\'' +
+ ", service='" + service + '\'' +
+ ", attestationData='" + attestationData.toString() + '\'' +
+ ", csr=" + csr.toString() +
+ '}';
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
new file mode 100644
index 00000000000..531a815922b
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
@@ -0,0 +1,198 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.restapi;
+
+import com.yahoo.component.annotation.Inject;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
+import com.yahoo.container.jdisc.secretstore.SecretStore;
+import com.yahoo.jdisc.http.server.jetty.RequestUtils;
+import com.yahoo.restapi.ErrorResponse;
+import com.yahoo.restapi.Path;
+import com.yahoo.restapi.SlimeJsonResponse;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SubjectAlternativeName;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.slime.Slime;
+import com.yahoo.slime.SlimeUtils;
+import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceConfirmation;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceValidator;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
+import com.yahoo.vespa.hosted.ca.Certificates;
+import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity;
+import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.time.Clock;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.logging.Level;
+
+/**
+ * REST API for issuing and refreshing node certificates in a hosted Vespa system.
+ *
+ * The API implements the following subset of methods from the Athenz ZTS REST API:
+ *
+ * - Instance registration
+ * - Instance refresh
+ *
+ * @author mpolden
+ */
+public class CertificateAuthorityApiHandler extends ThreadedHttpRequestHandler {
+
+ private final SecretStore secretStore;
+ private final Certificates certificates;
+ private final String caPrivateKeySecretName;
+ private final String caCertificateSecretName;
+ private final InstanceValidator instanceValidator;
+
+ @Inject
+ public CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, AthenzProviderServiceConfig athenzProviderServiceConfig, InstanceValidator instanceValidator) {
+ this(ctx, secretStore, new Certificates(Clock.systemUTC()), athenzProviderServiceConfig, instanceValidator);
+ }
+
+ CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, Certificates certificates, AthenzProviderServiceConfig athenzProviderServiceConfig, InstanceValidator instanceValidator) {
+ super(ctx);
+ this.secretStore = secretStore;
+ this.certificates = certificates;
+ this.caPrivateKeySecretName = athenzProviderServiceConfig.secretName();
+ this.caCertificateSecretName = athenzProviderServiceConfig.caCertSecretName();
+ this.instanceValidator = instanceValidator;
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ try {
+ switch (request.getMethod()) {
+ case POST: return handlePost(request);
+ default: return ErrorResponse.methodNotAllowed("Method " + request.getMethod() + " is unsupported");
+ }
+ } catch (IllegalArgumentException e) {
+ return ErrorResponse.badRequest(request.getMethod() + " " + request.getUri() + " failed: " + Exceptions.toMessageString(e));
+ } catch (RuntimeException e) {
+ log.log(Level.WARNING, "Unexpected error handling " + request.getMethod() + " " + request.getUri(), e);
+ return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
+ }
+ }
+
+ private HttpResponse handlePost(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/ca/v1/instance/")) return registerInstance(request);
+ if (path.matches("/ca/v1/instance/{provider}/{domain}/{service}/{instanceId}")) return refreshInstance(request, path.get("provider"), path.get("service"), path.get("instanceId"));
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ private HttpResponse registerInstance(HttpRequest request) {
+ var instanceRegistration = deserializeRequest(request, InstanceSerializer::registrationFromSlime);
+
+ InstanceConfirmation confirmation = new InstanceConfirmation(instanceRegistration.provider(), instanceRegistration.domain(), instanceRegistration.service(), EntityBindingsMapper.toSignedIdentityDocumentEntity(instanceRegistration.attestationData()));
+ confirmation.set(InstanceValidator.SAN_IPS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRegistration.csr(), SubjectAlternativeName.Type.IP));
+ confirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRegistration.csr(), SubjectAlternativeName.Type.DNS));
+ if (!instanceValidator.isValidInstance(confirmation)) {
+ log.log(Level.INFO, "Invalid instance registration for " + instanceRegistration.toString());
+ return ErrorResponse.forbidden("Unable to launch service: " +instanceRegistration.service());
+ }
+ var certificate = certificates.create(instanceRegistration.csr(), caCertificate(), caPrivateKey());
+ var instanceId = Certificates.instanceIdFrom(instanceRegistration.csr());
+ var identity = new InstanceIdentity(instanceRegistration.provider(), instanceRegistration.service(), instanceId,
+ Optional.of(certificate));
+ return new SlimeJsonResponse(InstanceSerializer.identityToSlime(identity));
+ }
+
+ private HttpResponse refreshInstance(HttpRequest request, String provider, String service, String instanceId) {
+ var instanceRefresh = deserializeRequest(request, InstanceSerializer::refreshFromSlime);
+ var instanceIdFromCsr = Certificates.instanceIdFrom(instanceRefresh.csr());
+
+ var athenzService = getRequestAthenzService(request);
+
+ if (!instanceIdFromCsr.equals(instanceId)) {
+ throw new IllegalArgumentException("Mismatch between instance ID in URL path and instance ID in CSR " +
+ "[instanceId=" + instanceId + ",instanceIdFromCsr=" + instanceIdFromCsr +
+ "]");
+ }
+
+ // Verify that the csr instance id matches one of the certificates in the chain
+ refreshesSameInstanceId(instanceIdFromCsr, request);
+
+
+ // Validate that there is no privilege escalation (can only refresh same service)
+ refreshesSameService(instanceRefresh, athenzService);
+
+ InstanceConfirmation instanceConfirmation = new InstanceConfirmation(provider, athenzService.getDomain().getName(), athenzService.getName(), null);
+ instanceConfirmation.set(InstanceValidator.SAN_IPS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRefresh.csr(), SubjectAlternativeName.Type.IP));
+ instanceConfirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRefresh.csr(), SubjectAlternativeName.Type.DNS));
+ if(!instanceValidator.isValidRefresh(instanceConfirmation)) {
+ return ErrorResponse.forbidden("Unable to refresh cert: " + instanceRefresh.csr().getSubject().toString());
+ }
+
+ var certificate = certificates.create(instanceRefresh.csr(), caCertificate(), caPrivateKey());
+ var identity = new InstanceIdentity(provider, service, instanceIdFromCsr, Optional.of(certificate));
+ return new SlimeJsonResponse(InstanceSerializer.identityToSlime(identity));
+ }
+
+ public void refreshesSameInstanceId(String csrInstanceId, HttpRequest request) {
+ String certificateInstanceId = getRequestCertificateChain(request).stream()
+ .map(Certificates::instanceIdFrom)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findAny().orElseThrow(() -> new IllegalArgumentException("No client certificate with instance id in request."));
+
+ if(! Objects.equals(certificateInstanceId, csrInstanceId)) {
+ throw new IllegalArgumentException("Mismatch between instance ID in client certificate and instance ID in CSR " +
+ "[instanceId=" + certificateInstanceId + ",instanceIdFromCsr=" + csrInstanceId +
+ "]");
+ }
+ }
+
+ private void refreshesSameService(InstanceRefresh instanceRefresh, AthenzService athenzService) {
+ List<String> commonNames = X509CertificateUtils.getCommonNames(instanceRefresh.csr().getSubject());
+ if(commonNames.size() != 1 && !Objects.equals(commonNames.get(0), athenzService.getFullName())) {
+ throw new IllegalArgumentException(String.format("Invalid request, trying to refresh service %s using service %s.", instanceRefresh.csr().getSubject().getName(), athenzService.getFullName()));
+ }
+ }
+
+ /** Returns CA certificate from secret store */
+ private X509Certificate caCertificate() {
+ return X509CertificateUtils.fromPem(secretStore.getSecret(caCertificateSecretName));
+ }
+
+ private List<X509Certificate> getRequestCertificateChain(HttpRequest request) {
+ return Optional.ofNullable(request.getJDiscRequest().context().get(RequestUtils.JDISC_REQUEST_X509CERT))
+ .map(X509Certificate[].class::cast)
+ .map(Arrays::asList)
+ .orElse(Collections.emptyList());
+ }
+
+ private AthenzService getRequestAthenzService(HttpRequest request) {
+ return getRequestCertificateChain(request).stream()
+ .findFirst()
+ .flatMap(X509CertificateUtils::getSubjectCommonName)
+ .map(AthenzService::new)
+ .orElseThrow(() -> new RuntimeException("No certificate found"));
+ }
+
+ /** Returns CA private key from secret store */
+ private PrivateKey caPrivateKey() {
+ return KeyUtils.fromPemEncodedPrivateKey(secretStore.getSecret(caPrivateKeySecretName));
+ }
+
+ private static <T> T deserializeRequest(HttpRequest request, Function<Slime, T> serializer) {
+ try {
+ var slime = SlimeUtils.jsonToSlime(request.getData().readAllBytes());
+ return serializer.apply(slime);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java
new file mode 100644
index 00000000000..fec03afab69
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java
@@ -0,0 +1,123 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.restapi;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.yahoo.security.Pkcs10CsrUtils;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.slime.ArrayTraverser;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.slime.SlimeUtils;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.identityprovider.api.ClusterType;
+import com.yahoo.vespa.athenz.identityprovider.api.IdentityType;
+import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
+import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
+import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity;
+import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh;
+import com.yahoo.vespa.hosted.ca.instance.InstanceRegistration;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author mpolden
+ */
+public class InstanceSerializer {
+
+ private static final String PROVIDER_FIELD = "provider";
+ private static final String DOMAIN_FIELD = "domain";
+ private static final String SERVICE_FIELD = "service";
+ private static final String ATTESTATION_DATA_FIELD = "attestationData";
+ private static final String CSR_FIELD = "csr";
+ private static final String NAME_FIELD = "service";
+ private static final String INSTANCE_ID_FIELD = "instanceId";
+ private static final String X509_CERTIFICATE_FIELD = "x509Certificate";
+
+ private static final String IDD_SIGNATURE_FIELD = "signature";
+ private static final String IDD_SIGNING_KEY_VERSION_FIELD = "signing-key-version";
+ private static final String IDD_PROVIDER_UNIQUE_ID_FIELD = "provider-unique-id";
+ private static final String IDD_PROVIDER_SERVICE_FIELD = "provider-service";
+ private static final String IDD_DOCUMENT_VERSION_FIELD = "document-version";
+ private static final String IDD_CONFIGSERVER_HOSTNAME_FIELD = "configserver-hostname";
+ private static final String IDD_INSTANCE_HOSTNAME_FIELD = "instance-hostname";
+ private static final String IDD_CREATED_AT_FIELD = "created-at";
+ private static final String IDD_IPADDRESSES_FIELD = "ip-addresses";
+ private static final String IDD_IDENTITY_TYPE_FIELD = "identity-type";
+ private static final String IDD_CLUSTER_TYPE_FIELD = "cluster-type";
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+ static {
+ objectMapper.registerModule(new JavaTimeModule());
+ }
+
+ private InstanceSerializer() {}
+
+ public static InstanceRegistration registrationFromSlime(Slime slime) {
+ Cursor root = slime.get();
+ return new InstanceRegistration(requireField(PROVIDER_FIELD, root).asString(),
+ requireField(DOMAIN_FIELD, root).asString(),
+ requireField(SERVICE_FIELD, root).asString(),
+ attestationDataToIdentityDocument(StringUtilities.unescape(requireField(ATTESTATION_DATA_FIELD, root).asString())),
+ Pkcs10CsrUtils.fromPem(requireField(CSR_FIELD, root).asString()));
+ }
+
+ public static InstanceRefresh refreshFromSlime(Slime slime) {
+ Cursor root = slime.get();
+ return new InstanceRefresh(Pkcs10CsrUtils.fromPem(requireField(CSR_FIELD, root).asString()));
+ }
+
+ public static Slime identityToSlime(InstanceIdentity identity) {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ root.setString(PROVIDER_FIELD, identity.provider());
+ root.setString(NAME_FIELD, identity.service());
+ root.setString(INSTANCE_ID_FIELD, identity.instanceId());
+ identity.x509Certificate()
+ .map(X509CertificateUtils::toPem)
+ .ifPresent(pem -> root.setString(X509_CERTIFICATE_FIELD, pem));
+ return slime;
+ }
+
+ public static SignedIdentityDocument attestationDataToIdentityDocument(String attestationData) {
+ Slime slime = SlimeUtils.jsonToSlime(attestationData);
+ Cursor root = slime.get();
+ String signature = requireField(IDD_SIGNATURE_FIELD, root).asString();
+ long signingKeyVersion = requireField(IDD_SIGNING_KEY_VERSION_FIELD, root).asLong();
+ VespaUniqueInstanceId providerUniqueId = VespaUniqueInstanceId.fromDottedString(requireField(IDD_PROVIDER_UNIQUE_ID_FIELD, root).asString());
+ AthenzService athenzService = new AthenzService(requireField(IDD_PROVIDER_SERVICE_FIELD, root).asString());
+ long documentVersion = requireField(IDD_DOCUMENT_VERSION_FIELD, root).asLong();
+ String configserverHostname = requireField(IDD_CONFIGSERVER_HOSTNAME_FIELD, root).asString();
+ String instanceHostname = requireField(IDD_INSTANCE_HOSTNAME_FIELD, root).asString();
+ double createdAtTimestamp = requireField(IDD_CREATED_AT_FIELD, root).asDouble();
+ Instant createdAt = getJsr310Instant(createdAtTimestamp);
+ Set<String> ips = new HashSet<>();
+ requireField(IDD_IPADDRESSES_FIELD, root).traverse((ArrayTraverser) (__, entry) -> ips.add(entry.asString()));
+ IdentityType identityType = IdentityType.fromId(requireField(IDD_IDENTITY_TYPE_FIELD, root).asString());
+ var clusterTypeField = root.field(IDD_CLUSTER_TYPE_FIELD);
+ var clusterType = clusterTypeField.valid() ? ClusterType.from(clusterTypeField.asString()) : null;
+
+
+ return new SignedIdentityDocument(signature, (int)signingKeyVersion, providerUniqueId, athenzService, (int)documentVersion,
+ configserverHostname, instanceHostname, createdAt, ips, identityType, clusterType);
+ }
+
+ private static Instant getJsr310Instant(double v) {
+ try {
+ return objectMapper.readValue(Double.toString(v), Instant.class);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Cursor requireField(String fieldName, Cursor root) {
+ var field = root.field(fieldName);
+ if (!field.valid()) throw new IllegalArgumentException("Missing required field '" + fieldName + "'");
+ return field;
+ }
+
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/package-info.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/package-info.java
new file mode 100644
index 00000000000..118f4b08c2a
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/package-info.java
@@ -0,0 +1,8 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author mpolden
+ */
+@ExportPackage
+package com.yahoo.vespa.hosted.ca.restapi;
+
+import com.yahoo.osgi.annotation.ExportPackage;
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..67e5caa0c18
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AutoGeneratedKeyProvider.java
@@ -0,0 +1,37 @@
+// Copyright Yahoo. 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.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyUtils;
+
+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() {
+ keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048);
+ }
+
+ @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/IdentityDocumentGeneratorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGeneratorTest.java
new file mode 100644
index 00000000000..9205baff0fc
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGeneratorTest.java
@@ -0,0 +1,102 @@
+// Copyright Yahoo. 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.yahoo.component.Version;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.InstanceName;
+import com.yahoo.config.provision.NodeResources;
+import com.yahoo.config.provision.NodeType;
+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.athenz.identityprovider.api.IdentityType;
+import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
+import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
+import com.yahoo.vespa.athenz.identityprovider.client.IdentityDocumentSigner;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
+import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeRepository;
+import com.yahoo.vespa.hosted.provision.node.Allocation;
+import com.yahoo.vespa.hosted.provision.node.Generation;
+import com.yahoo.vespa.hosted.provision.node.IP;
+import com.yahoo.vespa.hosted.provision.node.Nodes;
+import com.yahoo.vespa.hosted.provision.testutils.MockNodeFlavors;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+import java.util.Set;
+
+import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.TestUtils.getAthenzProviderConfig;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author valerijf
+ */
+public class IdentityDocumentGeneratorTest {
+
+ private static final Zone ZONE = new Zone(SystemName.cd, Environment.dev, RegionName.from("us-north-1"));
+
+ @Test
+ void generates_valid_identity_document() {
+ String parentHostname = "docker-host";
+ String containerHostname = "docker-container";
+
+ ApplicationId appid = ApplicationId.from(
+ TenantName.from("tenant"), ApplicationName.from("application"), InstanceName.from("default"));
+ Allocation allocation = new Allocation(appid,
+ ClusterMembership.from("container/default/0/0", Version.fromString("1.2.3"), Optional.empty()),
+ new NodeResources(1, 1, 1, 1),
+ Generation.initial(),
+ false);
+ Node parentNode = Node.create("ostkid",
+ IP.Config.ofEmptyPool(Set.of("127.0.0.1")),
+ parentHostname,
+ new MockNodeFlavors().getFlavorOrThrow("default"),
+ NodeType.host).build();
+ Node containerNode = Node.reserve(Set.of("::1"),
+ containerHostname,
+ parentHostname,
+ new MockNodeFlavors().getFlavorOrThrow("default").resources(),
+ NodeType.tenant)
+ .allocation(allocation).build();
+ NodeRepository nodeRepository = mock(NodeRepository.class);
+ Nodes nodes = mock(Nodes.class);
+ when(nodeRepository.nodes()).thenReturn(nodes);
+
+ when(nodes.node(eq(parentHostname))).thenReturn(Optional.of(parentNode));
+ when(nodes.node(eq(containerHostname))).thenReturn(Optional.of(containerNode));
+ AutoGeneratedKeyProvider keyProvider = new AutoGeneratedKeyProvider();
+
+ String dnsSuffix = "vespa.dns.suffix";
+ AthenzProviderServiceConfig config = getAthenzProviderConfig("domain", "service", dnsSuffix);
+ IdentityDocumentGenerator identityDocumentGenerator =
+ new IdentityDocumentGenerator(config, nodeRepository, ZONE, keyProvider);
+ SignedIdentityDocument signedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(containerHostname, IdentityType.TENANT);
+
+ // Verify attributes
+ assertEquals(containerHostname, signedIdentityDocument.instanceHostname());
+
+ String environment = "dev";
+ String region = "us-north-1";
+
+ VespaUniqueInstanceId expectedProviderUniqueId =
+ new VespaUniqueInstanceId(0, "default", "default", "application", "tenant", region, environment, IdentityType.TENANT);
+ assertEquals(expectedProviderUniqueId, signedIdentityDocument.providerUniqueId());
+
+ // Validate that container ips are present
+ assertTrue(signedIdentityDocument.ipAddresses().contains("::1"));
+
+ IdentityDocumentSigner signer = new IdentityDocumentSigner();
+
+ // Validate signature
+ assertTrue(signer.hasValidSignature(signedIdentityDocument, keyProvider.getPublicKey(0)));
+ }
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidatorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidatorTest.java
new file mode 100644
index 00000000000..a7947aff283
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidatorTest.java
@@ -0,0 +1,296 @@
+// Copyright Yahoo. 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.yahoo.component.Version;
+import com.yahoo.config.model.api.ApplicationInfo;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.model.api.Model;
+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.config.provision.ClusterMembership;
+import com.yahoo.config.provision.NodeResources;
+import com.yahoo.config.provision.NodeType;
+import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.identityprovider.api.ClusterType;
+import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper;
+import com.yahoo.vespa.athenz.identityprovider.api.IdentityType;
+import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
+import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
+import com.yahoo.vespa.athenz.identityprovider.client.IdentityDocumentSigner;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceValidator.ValidationException;
+import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeList;
+import com.yahoo.vespa.hosted.provision.NodeRepository;
+import com.yahoo.vespa.hosted.provision.node.IP;
+import com.yahoo.vespa.hosted.provision.node.Nodes;
+import com.yahoo.vespa.hosted.provision.testutils.MockNodeFlavors;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceValidator.SERVICE_PROPERTIES_DOMAIN_KEY;
+import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceValidator.SERVICE_PROPERTIES_SERVICE_KEY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author valerijf
+ * @author bjorncs
+ * @author mortent
+ */
+public class InstanceValidatorTest {
+
+ private final ApplicationId applicationId = ApplicationId.from("tenant", "application", "instance");
+ private final String domain = "domain";
+ private final String service = "service";
+
+ private final AthenzService vespaTenantDomain = new AthenzService("vespa.vespa.tenant");
+ private final AutoGeneratedKeyProvider keyProvider = new AutoGeneratedKeyProvider();
+
+ @Test
+ void application_does_not_exist() {
+ SuperModelProvider superModelProvider = mockSuperModelProvider();
+ InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain);
+ assertFalse(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service)));
+ }
+
+ @Test
+ void application_does_not_have_domain_set() {
+ SuperModelProvider superModelProvider = mockSuperModelProvider(
+ mockApplicationInfo(applicationId, 5, Collections.emptyList()));
+ InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, new IdentityDocumentSigner(), vespaTenantDomain);
+
+ assertFalse(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service)));
+ }
+
+ @Test
+ void application_has_wrong_domain() {
+ ServiceInfo serviceInfo = new ServiceInfo("serviceName", "type", Collections.emptyList(),
+ Collections.singletonMap(SERVICE_PROPERTIES_DOMAIN_KEY, "not-domain"), "confId", "hostName");
+
+ SuperModelProvider superModelProvider = mockSuperModelProvider(
+ mockApplicationInfo(applicationId, 5, Collections.singletonList(serviceInfo)));
+ InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain);
+
+ assertFalse(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service)));
+ }
+
+ @Test
+ void application_has_same_domain_and_service() {
+ Map<String, String> properties = new HashMap<>();
+ properties.put(SERVICE_PROPERTIES_DOMAIN_KEY, domain);
+ properties.put(SERVICE_PROPERTIES_SERVICE_KEY, service);
+
+ ServiceInfo serviceInfo = new ServiceInfo("serviceName", "type", Collections.emptyList(),
+ properties, "confId", "hostName");
+
+ SuperModelProvider superModelProvider = mockSuperModelProvider(
+ mockApplicationInfo(applicationId, 5, Collections.singletonList(serviceInfo)));
+ IdentityDocumentSigner signer = mock(IdentityDocumentSigner.class);
+ when(signer.hasValidSignature(any(), any())).thenReturn(true);
+ InstanceValidator instanceValidator = new InstanceValidator(mock(KeyProvider.class), superModelProvider, mockNodeRepo(), signer, vespaTenantDomain);
+
+ assertTrue(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service)));
+ }
+
+ @Test
+ void rejects_invalid_provider_unique_id_in_csr() {
+ SuperModelProvider superModelProvider = mockSuperModelProvider();
+ InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain);
+ InstanceConfirmation instanceConfirmation = createRegisterInstanceConfirmation(applicationId, domain, service);
+ VespaUniqueInstanceId tamperedId = new VespaUniqueInstanceId(0, "default", "instance", "app", "tenant", "us-north-1", "dev", IdentityType.NODE);
+ instanceConfirmation.set("sanDNS", tamperedId.asDottedString() + ".instanceid.athenz.dev-us-north-1.vespa.yahoo.cloud");
+ assertFalse(instanceValidator.isValidInstance(instanceConfirmation));
+ }
+
+ @Test
+ void rejects_unknown_ips_in_csr() {
+ NodeRepository nodeRepository = mockNodeRepo();
+ InstanceValidator instanceValidator = new InstanceValidator(null, mockSuperModelProvider(), nodeRepository, null, vespaTenantDomain);
+ InstanceConfirmation instanceConfirmation = createRegisterInstanceConfirmation(applicationId, domain, service);
+ Set<String> nodeIp = nodeRepository.nodes().list().owner(applicationId).stream().findFirst()
+ .map(Node::ipConfig)
+ .map(IP.Config::primary)
+ .orElseThrow(() -> new RuntimeException("No ipaddress for mocked node"));
+
+ List<String> ips = new ArrayList<>(nodeIp);
+ ips.add("::ff");
+ instanceConfirmation.set("sanIP", String.join(",", ips));
+ assertFalse(instanceValidator.isValidInstance(instanceConfirmation));
+ }
+
+ @Test
+ void rejects_invalid_cluster_type_in_csr() {
+ var props = Map.of(SERVICE_PROPERTIES_DOMAIN_KEY, domain, SERVICE_PROPERTIES_SERVICE_KEY, service);
+ var info = new ServiceInfo("serviceName", "type", List.of(), props, "confId", "hostName");
+ var provider = mockSuperModelProvider(mockApplicationInfo(applicationId, 5, List.of(info)));
+ var instanceValidator = new InstanceValidator(keyProvider, provider, mockNodeRepo(), new IdentityDocumentSigner(), vespaTenantDomain);
+ var instanceConfirmation = createRegisterInstanceConfirmation(applicationId, domain, service);
+ instanceConfirmation.set("sanURI", "vespa://cluster-type/content");
+ var exception = assertThrows(ValidationException.class, () -> instanceValidator.validateInstance(instanceConfirmation));
+ var expectedMsg = "Illegal SAN URIs: expected '[vespa://cluster-type/container]' found '[vespa://cluster-type/content]'";
+ assertEquals(expectedMsg, exception.getMessage());
+ }
+
+ @Test
+ void accepts_valid_refresh_requests() {
+ NodeRepository nodeRepository = mock(NodeRepository.class);
+ Nodes nodes = mock(Nodes.class);
+ when(nodeRepository.nodes()).thenReturn(nodes);
+ InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain);
+
+ List<Node> nodeList = createNodes(10);
+ Node node = nodeList.get(0);
+ nodeList = allocateNode(nodeList, node, applicationId);
+ when(nodes.list()).thenReturn(NodeList.copyOf(nodeList));
+ String nodeIp = node.ipConfig().primary().stream().findAny().orElseThrow(() -> new RuntimeException("No ipaddress for mocked node"));
+ InstanceConfirmation instanceConfirmation = createRefreshInstanceConfirmation(applicationId, domain, service, List.of(nodeIp));
+
+ assertTrue(instanceValidator.isValidRefresh(instanceConfirmation));
+ }
+
+ @Test
+ void rejects_refresh_on_ip_mismatch() {
+ NodeRepository nodeRepository = mockNodeRepo();
+ InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain);
+
+ Set<String> nodeIp = nodeRepository.nodes().list().owner(applicationId).stream().findFirst()
+ .map(Node::ipConfig)
+ .map(IP.Config::primary)
+ .orElseThrow(() -> new RuntimeException("No ipaddress for mocked node"));
+
+ List<String> ips = new ArrayList<>(nodeIp);
+ ips.add("::ff");
+ // Add invalid ip to list of ip addresses
+ InstanceConfirmation instanceConfirmation = createRefreshInstanceConfirmation(applicationId, domain, service, ips);
+
+ assertFalse(instanceValidator.isValidRefresh(instanceConfirmation));
+ }
+
+ @Test
+ void rejects_refresh_when_node_is_not_allocated() {
+ NodeRepository nodeRepository = mock(NodeRepository.class);
+ Nodes nodes = mock(Nodes.class);
+ when(nodeRepository.nodes()).thenReturn(nodes);
+
+ InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain);
+
+ List<Node> nodeList = createNodes(10);
+
+ when(nodes.list()).thenReturn(NodeList.copyOf(nodeList));
+ InstanceConfirmation instanceConfirmation = createRefreshInstanceConfirmation(applicationId, domain, service, List.of("::11"));
+
+ assertFalse(instanceValidator.isValidRefresh(instanceConfirmation));
+
+ }
+
+ private NodeRepository mockNodeRepo() {
+ NodeRepository nodeRepository = mock(NodeRepository.class);
+ Nodes nodes = mock(Nodes.class);
+ when(nodeRepository.nodes()).thenReturn(nodes);
+ List<Node> nodeList = createNodes(10);
+ Node node = nodeList.get(0);
+ nodeList = allocateNode(nodeList, node, applicationId);
+ when(nodes.list()).thenReturn(NodeList.copyOf(nodeList));
+ return nodeRepository;
+ }
+
+ private InstanceConfirmation createRegisterInstanceConfirmation(
+ ApplicationId applicationId, String domain, String service) {
+ VespaUniqueInstanceId vespaUniqueInstanceId = new VespaUniqueInstanceId(0, "default", applicationId.instance().value(), applicationId.application().value(), applicationId.tenant().value(), "us-north-1", "dev", IdentityType.NODE);
+ var domainService = new AthenzService(domain, service);
+ var clock = Instant.now();
+ var clusterType = ClusterType.CONTAINER;
+ var signature = new IdentityDocumentSigner()
+ .generateSignature(
+ vespaUniqueInstanceId, domainService, "localhost", "localhost", clock, Set.of(),
+ IdentityType.NODE, keyProvider.getPrivateKey(0));
+ SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument(
+ signature, 0, vespaUniqueInstanceId, domainService, 0, "localhost", "localhost",
+ clock, Collections.emptySet(), IdentityType.NODE, clusterType);
+ return createInstanceConfirmation(vespaUniqueInstanceId, domain, service, signedIdentityDocument);
+ }
+
+ private InstanceConfirmation createRefreshInstanceConfirmation(ApplicationId applicationId, String domain, String service, List<String> ips) {
+ VespaUniqueInstanceId vespaUniqueInstanceId = new VespaUniqueInstanceId(0, "default", applicationId.instance().value(), applicationId.application().value(), applicationId.tenant().value(), "us-north-1", "dev", IdentityType.NODE);
+ InstanceConfirmation instanceConfirmation = createInstanceConfirmation(vespaUniqueInstanceId, domain, service, null);
+ instanceConfirmation.set("sanIP", String.join(",", ips));
+ return instanceConfirmation;
+ }
+
+ private InstanceConfirmation createInstanceConfirmation(VespaUniqueInstanceId vespaUniqueInstanceId, String domain, String service, SignedIdentityDocument identityDocument) {
+ InstanceConfirmation instanceConfirmation = new InstanceConfirmation(
+ "vespa.vespa.cd.provider_dev_us-north-1",
+ domain,
+ service,
+ Optional.ofNullable(identityDocument)
+ .map(EntityBindingsMapper::toSignedIdentityDocumentEntity)
+ .orElse(null));
+ instanceConfirmation.set("sanDNS", vespaUniqueInstanceId.asDottedString() + ".instanceid.athenz.dev-us-north-1.vespa.yahoo.cloud");
+ instanceConfirmation.set("sanURI", "vespa://cluster-type/container");
+ return instanceConfirmation;
+ }
+
+ private SuperModelProvider mockSuperModelProvider(ApplicationInfo... appInfos) {
+ SuperModel superModel = new SuperModel(Stream.of(appInfos)
+ .collect(Collectors.toMap(
+ ApplicationInfo::getApplicationId,
+ Function.identity()
+ )
+ ),
+ true);
+
+ SuperModelProvider superModelProvider = mock(SuperModelProvider.class);
+ when(superModelProvider.getSuperModel()).thenReturn(superModel);
+ return superModelProvider;
+ }
+
+ private ApplicationInfo mockApplicationInfo(ApplicationId appId, int numHosts, List<ServiceInfo> serviceInfo) {
+ List<HostInfo> hosts = IntStream.range(0, numHosts)
+ .mapToObj(i -> new HostInfo("host-" + i + "." + appId.toShortString() + ".yahoo.com", serviceInfo))
+ .toList();
+
+ Model model = mock(Model.class);
+ when(model.getHosts()).thenReturn(hosts);
+
+ return new ApplicationInfo(appId, 0, model);
+ }
+
+ private List<Node> createNodes(int num) {
+ MockNodeFlavors flavors = new MockNodeFlavors();
+ List<Node> nodeList = new ArrayList<>();
+ for (int i = 0; i < num; i++) {
+ Node node = Node.create("foo" + i, new IP.Config(Set.of("::1" + i, "::2" + i, "::3" + i), Set.of()),
+ "foo" + i, flavors.getFlavorOrThrow("default"), NodeType.tenant).build();
+ nodeList.add(node);
+ }
+ return nodeList;
+ }
+
+ private List<Node> allocateNode(List<Node> nodeList, Node node, ApplicationId applicationId) {
+ nodeList.removeIf(n -> n.id().equals(node.id()));
+ nodeList.add(node.allocate(applicationId,
+ ClusterMembership.from("container/default/0/0", Version.fromString("6.123.4"), Optional.empty()),
+ new NodeResources(1, 1, 1, 1),
+ Instant.now()));
+ return nodeList;
+ }
+}
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..4110ad2bfa2
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/TestUtils.java
@@ -0,0 +1,27 @@
+// Copyright Yahoo. 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.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
+
+/**
+ * @author bjorncs
+ */
+public class TestUtils {
+
+ public static AthenzProviderServiceConfig getAthenzProviderConfig(String domain,
+ String service,
+ String dnsSuffix) {
+ AthenzProviderServiceConfig.Builder zoneConfig =
+ new AthenzProviderServiceConfig.Builder()
+ .serviceName(service)
+ .secretVersion(0)
+ .domain(domain)
+ .certDnsSuffix(dnsSuffix)
+ .ztsUrl("localhost/zts")
+ .secretName("s3cr3t")
+ .caCertSecretName(domain + ".ca.cert");
+ return new AthenzProviderServiceConfig(
+ zoneConfig.athenzCaTrustStore("/dummy/path/to/athenz-ca.jks"));
+ }
+
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificateTester.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificateTester.java
new file mode 100644
index 00000000000..4012776949e
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificateTester.java
@@ -0,0 +1,79 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca;
+
+import com.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.Pkcs10Csr;
+import com.yahoo.security.Pkcs10CsrBuilder;
+import com.yahoo.security.SignatureAlgorithm;
+import com.yahoo.security.SubjectAlternativeName;
+import com.yahoo.security.X509CertificateBuilder;
+
+import javax.security.auth.x500.X500Principal;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+
+import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA;
+
+/**
+ * Helper class for creating certificates, CSRs etc. for testing purposes.
+ *
+ * @author mpolden
+ */
+public class CertificateTester {
+
+ private CertificateTester() {}
+
+ public static X509Certificate createCertificate() {
+ var keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ return createCertificate("subject", keyPair);
+ }
+
+ public static X509Certificate createCertificate(String cn, KeyPair keyPair) {
+ var subject = new X500Principal("CN=" + cn);
+ return X509CertificateBuilder.fromKeypair(keyPair,
+ subject,
+ Instant.EPOCH,
+ Instant.EPOCH.plus(Duration.ofMinutes(1)),
+ SHA256_WITH_ECDSA,
+ BigInteger.ONE)
+ .build();
+ }
+
+ public static Pkcs10Csr createCsr() {
+ return createCsr(List.of(), List.of());
+ }
+
+ public static Pkcs10Csr createCsr(String dnsName) {
+ return createCsr(List.of(dnsName), List.of());
+ }
+
+ public static Pkcs10Csr createCsr(List<String> dnsNames) {
+ return createCsr(dnsNames, List.of());
+ }
+
+ public static Pkcs10Csr createCsr(String cn, List<String> dnsNames) {
+ return createCsr(cn, dnsNames, List.of());
+ }
+
+ public static Pkcs10Csr createCsr(List<String> dnsNames, List<String> ipAddresses) {
+ return createCsr("subject", dnsNames, ipAddresses);
+ }
+ public static Pkcs10Csr createCsr(String cn, List<String> dnsNames, List<String> ipAddresses) {
+ X500Principal subject = new X500Principal("CN=" + cn);
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ var builder = Pkcs10CsrBuilder.fromKeypair(subject, keyPair, SignatureAlgorithm.SHA512_WITH_ECDSA);
+ for (var dnsName : dnsNames) {
+ builder = builder.addSubjectAlternativeName(SubjectAlternativeName.Type.DNS, dnsName);
+ }
+ for (var ipAddress : ipAddresses) {
+ builder = builder.addSubjectAlternativeName(SubjectAlternativeName.Type.IP, ipAddress);
+ }
+ return builder.build();
+ }
+
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificatesTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificatesTest.java
new file mode 100644
index 00000000000..dd3ddeeb804
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificatesTest.java
@@ -0,0 +1,65 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca;
+
+import com.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SubjectAlternativeName;
+import com.yahoo.test.ManualClock;
+import org.junit.jupiter.api.Test;
+
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.List;
+
+import static java.time.temporal.ChronoUnit.SECONDS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * @author mpolden
+ */
+public class CertificatesTest {
+
+ private final KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ private final X509Certificate caCertificate = CertificateTester.createCertificate("CA", keyPair);
+
+ @Test
+ void expiry() {
+ var clock = new ManualClock();
+ var certificates = new Certificates(clock);
+ var csr = CertificateTester.createCsr();
+ var certificate = certificates.create(csr, caCertificate, keyPair.getPrivate());
+ var now = clock.instant();
+
+ assertEquals(now.minus(Duration.ofHours(1)).truncatedTo(SECONDS), certificate.getNotBefore().toInstant());
+ assertEquals(now.plus(Duration.ofDays(30)).truncatedTo(SECONDS), certificate.getNotAfter().toInstant());
+ }
+
+ @Test
+ void add_san_from_csr() throws Exception {
+ var certificates = new Certificates(new ManualClock());
+ var dnsName = "host.example.com";
+ var ip = "192.0.2.42";
+ var csr = CertificateTester.createCsr(List.of(dnsName), List.of(ip));
+ var certificate = certificates.create(csr, caCertificate, keyPair.getPrivate());
+
+ assertNotNull(certificate.getSubjectAlternativeNames());
+ assertEquals(2, certificate.getSubjectAlternativeNames().size());
+
+ var subjectAlternativeNames = List.copyOf(certificate.getSubjectAlternativeNames());
+ assertEquals(List.of(SubjectAlternativeName.Type.DNS.getTag(), dnsName),
+ subjectAlternativeNames.get(0));
+ assertEquals(List.of(SubjectAlternativeName.Type.IP.getTag(), ip),
+ subjectAlternativeNames.get(1));
+ }
+
+ @Test
+ void parse_instance_id() {
+ var instanceId = "1.cluster1.default.app1.tenant1.us-north-1.prod.node";
+ var instanceIdWithSuffix = instanceId + ".instanceid.athenz.dev-us-north-1.vespa.aws.oath.cloud";
+ var csr = CertificateTester.createCsr(List.of("foo", "bar", instanceIdWithSuffix));
+ assertEquals(instanceId, Certificates.instanceIdFrom(csr));
+ }
+
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java
new file mode 100644
index 00000000000..bf2115e8759
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiTest.java
@@ -0,0 +1,243 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.restapi;
+
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.jdisc.http.server.jetty.RequestUtils;
+import com.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.Pkcs10Csr;
+import com.yahoo.security.Pkcs10CsrUtils;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.slime.SlimeUtils;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.athenz.api.AthenzPrincipal;
+import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.client.ErrorHandler;
+import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient;
+import com.yahoo.vespa.hosted.ca.CertificateTester;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import javax.net.ssl.SSLContext;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author mpolden
+ */
+public class CertificateAuthorityApiTest extends ContainerTester {
+
+ private static final String INSTANCE_ID = "1.cluster1.default.app1.tenant1.us-north-1.prod.node";
+ private static final String INSTANCE_ID_WITH_SUFFIX = INSTANCE_ID + ".instanceid.athenz.dev-us-north-1.vespa.aws.oath.cloud";
+ private static final String INVALID_INSTANCE_ID = "1.cluster1.default.otherapp.othertenant.us-north-1.prod.node";
+ private static final String INVALID_INSTANCE_ID_WITH_SUFFIX = INVALID_INSTANCE_ID + ".instanceid.athenz.dev-us-north-1.vespa.aws.oath.cloud";
+
+ private static final String CONTAINER_IDENTITY = "vespa.external.tenant";
+ private static final String HOST_IDENTITY = "vespa.external.tenant-host";
+
+ @BeforeEach
+ public void before() {
+ setCaCertificateAndKey();
+ }
+
+ @Test
+ void register_instance() throws Exception {
+ // POST instance registration
+ var csr = CertificateTester.createCsr(List.of("node1.example.com", INSTANCE_ID_WITH_SUFFIX));
+ assertIdentityResponse(new Request("http://localhost:12345/ca/v1/instance/",
+ instanceRegistrationJson(csr),
+ Request.Method.POST));
+
+ // POST instance registration with ZTS client
+ var ztsClient = new TestZtsClient(new AthenzPrincipal(new AthenzService(HOST_IDENTITY)), null, URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault());
+ var instanceIdentity = ztsClient.registerInstance(new AthenzService("vespa.external", "provider_prod_us-north-1"),
+ new AthenzService(CONTAINER_IDENTITY),
+ getAttestationData(),
+ csr);
+ assertEquals("CN=Vespa CA", instanceIdentity.certificate().getIssuerX500Principal().getName());
+ }
+
+ private X509Certificate registerInstance() throws Exception {
+ // POST instance registration
+ var csr = CertificateTester.createCsr(CONTAINER_IDENTITY, List.of("node1.example.com", INSTANCE_ID_WITH_SUFFIX));
+ assertIdentityResponse(new Request("http://localhost:12345/ca/v1/instance/",
+ instanceRegistrationJson(csr),
+ Request.Method.POST));
+
+ // POST instance registration with ZTS client
+ var ztsClient = new TestZtsClient(new AthenzPrincipal(new AthenzService(HOST_IDENTITY)), null, URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault());
+ var instanceIdentity = ztsClient.registerInstance(new AthenzService("vespa.external", "provider_prod_us-north-1"),
+ new AthenzService(CONTAINER_IDENTITY),
+ getAttestationData(),
+ csr);
+ return instanceIdentity.certificate();
+ }
+
+ @Test
+ void refresh_instance() throws Exception {
+ // Register instance to get cert
+ var certificate = registerInstance();
+
+ // POST instance refresh
+ var principal = new AthenzPrincipal(new AthenzService(CONTAINER_IDENTITY));
+ var csr = CertificateTester.createCsr(principal.getIdentity().getFullName(), List.of("node1.example.com", INSTANCE_ID_WITH_SUFFIX));
+ var request = new Request("http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/" + INSTANCE_ID,
+ instanceRefreshJson(csr),
+ Request.Method.POST,
+ principal);
+ request.getAttributes().put(RequestUtils.JDISC_REQUEST_X509CERT, new X509Certificate[]{certificate});
+ assertIdentityResponse(request);
+
+ // POST instance refresh with ZTS client
+ var ztsClient = new TestZtsClient(principal, certificate, URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault());
+ var instanceIdentity = ztsClient.refreshInstance(new AthenzService("vespa.external", "provider_prod_us-north-1"),
+ new AthenzService(CONTAINER_IDENTITY),
+ INSTANCE_ID,
+ csr);
+ assertEquals("CN=Vespa CA", instanceIdentity.certificate().getIssuerX500Principal().getName());
+ }
+
+ @Test
+ void invalid_requests() throws Exception {
+ // POST instance registration with missing fields
+ assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"POST http://localhost:12345/ca/v1/instance/ failed: Missing required field 'provider'\"}",
+ new Request("http://localhost:12345/ca/v1/instance/",
+ new byte[0],
+ Request.Method.POST));
+
+ // POST instance registration without DNS name in CSR
+ var csr = CertificateTester.createCsr();
+ var request = new Request("http://localhost:12345/ca/v1/instance/",
+ instanceRegistrationJson(csr),
+ Request.Method.POST);
+ assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"POST http://localhost:12345/ca/v1/instance/ failed: No instance ID found in CSR\"}", request);
+
+ // POST instance refresh with missing field
+ assertResponse(400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"POST http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/1.cluster1.default.app1.tenant1.us-north-1.prod.node failed: Missing required field 'csr'\"}",
+ new Request("http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/" + INSTANCE_ID,
+ new byte[0],
+ Request.Method.POST));
+
+ // POST instance refresh where instanceId does not match CSR dnsName
+ var principal = new AthenzPrincipal(new AthenzService(CONTAINER_IDENTITY));
+ var cert = CertificateTester.createCertificate(CONTAINER_IDENTITY, KeyUtils.generateKeypair(KeyAlgorithm.EC));
+ csr = CertificateTester.createCsr(principal.getIdentity().getFullName(), List.of("node1.example.com", INSTANCE_ID_WITH_SUFFIX));
+ request = new Request("http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/foobar",
+ instanceRefreshJson(csr),
+ Request.Method.POST,
+ principal);
+ request.getAttributes().put(RequestUtils.JDISC_REQUEST_X509CERT, new X509Certificate[]{cert});
+ assertResponse(
+ 400,
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"POST http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/foobar failed: Mismatch between instance ID in URL path and instance ID in CSR [instanceId=foobar,instanceIdFromCsr=1.cluster1.default.app1.tenant1.us-north-1.prod.node]\"}",
+ request);
+
+ // POST instance refresh using zts client where client cert does not contain instanceid
+ var certificate = registerInstance();
+ var ztsClient = new TestZtsClient(principal, certificate, URI.create("http://localhost:12345/ca/v1/"), SSLContext.getDefault());
+ try {
+ var invalidCsr = CertificateTester.createCsr(principal.getIdentity().getFullName(), List.of("node1.example.com", INVALID_INSTANCE_ID_WITH_SUFFIX));
+ var instanceIdentity = ztsClient.refreshInstance(new AthenzService("vespa.external", "provider_prod_us-north-1"),
+ new AthenzService(CONTAINER_IDENTITY),
+ INSTANCE_ID,
+ invalidCsr);
+ fail("Refresh instance should have failed");
+ } catch (Exception e) {
+ String expectedMessage = "Received error from ZTS: code=0, message=\"POST http://localhost:12345/ca/v1/instance/vespa.external.provider_prod_us-north-1/vespa.external/tenant/1.cluster1.default.app1.tenant1.us-north-1.prod.node failed: Mismatch between instance ID in URL path and instance ID in CSR [instanceId=1.cluster1.default.app1.tenant1.us-north-1.prod.node,instanceIdFromCsr=1.cluster1.default.otherapp.othertenant.us-north-1.prod.node]\"";
+ assertEquals(expectedMessage, e.getMessage());
+ }
+ }
+
+ private void setCaCertificateAndKey() {
+ var keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ var caCertificatePem = X509CertificateUtils.toPem(CertificateTester.createCertificate("Vespa CA", keyPair));
+ var privateKeyPem = KeyUtils.toPem(keyPair.getPrivate());
+ secretStore().setSecret("vespa.external.ca.cert", caCertificatePem)
+ .setSecret("secretname", privateKeyPem);
+ }
+
+ private void assertIdentityResponse(Request request) {
+ assertResponse(200, (body) -> {
+ var slime = SlimeUtils.jsonToSlime(body);
+ var root = slime.get();
+ assertEquals("vespa.external.provider_prod_us-north-1", root.field("provider").asString());
+ assertEquals("tenant", root.field("service").asString());
+ assertEquals(INSTANCE_ID, root.field("instanceId").asString());
+ var pemEncodedCertificate = root.field("x509Certificate").asString();
+ assertTrue(pemEncodedCertificate.startsWith("-----BEGIN CERTIFICATE-----") &&
+ pemEncodedCertificate.endsWith("-----END CERTIFICATE-----\n"),
+ "Response contains PEM certificate");
+ }, request);
+ }
+
+ private static byte[] instanceRefreshJson(Pkcs10Csr csr) {
+ var csrPem = Pkcs10CsrUtils.toPem(csr);
+ var json = "{\"csr\": \"" + csrPem + "\"}";
+ return json.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static byte[] instanceRegistrationJson(Pkcs10Csr csr) {
+ var csrPem = Pkcs10CsrUtils.toPem(csr);
+ var json = "{\n" +
+ " \"provider\": \"vespa.external.provider_prod_us-north-1\",\n" +
+ " \"domain\": \"vespa.external\",\n" +
+ " \"service\": \"tenant\",\n" +
+ " \"attestationData\": \""+getAttestationData()+"\",\n" +
+ " \"csr\": \"" + csrPem + "\"\n" +
+ "}";
+ return json.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static String getAttestationData () {
+ var json = "{\n" +
+ " \"signature\": \"SIGNATURE\",\n" +
+ " \"signing-key-version\": 0,\n" +
+ " \"provider-unique-id\": \"0.default.default.application.tenant.us-north-1.dev.tenant\",\n" +
+ " \"provider-service\": \"domain.service\",\n" +
+ " \"document-version\": 1,\n" +
+ " \"configserver-hostname\": \"localhost\",\n" +
+ " \"instance-hostname\": \"docker-container\",\n" +
+ " \"created-at\": 1572000079.00000,\n" +
+ " \"ip-addresses\": [\n" +
+ " \"::1\"\n" +
+ " ],\n" +
+ " \"identity-type\": \"tenant\"\n" +
+ "}";
+ return StringUtilities.escape(json);
+ }
+
+ /*
+ Zts client that adds principal as header (since setting up ssl in test is cumbersome)
+ */
+ private static class TestZtsClient extends DefaultZtsClient {
+
+ private final Principal principal;
+ private final X509Certificate certificate;
+
+ public TestZtsClient(Principal principal, X509Certificate certificate, URI ztsUrl, SSLContext sslContext) {
+ super(ztsUrl, () -> sslContext, null, ErrorHandler.empty());
+ this.principal = principal;
+ this.certificate = certificate;
+ }
+
+ @Override
+ protected <T> T execute(HttpUriRequest request, ResponseHandler<T> responseHandler) {
+ request.addHeader("PRINCIPAL", principal.getName());
+ Optional.ofNullable(certificate).ifPresent(cert -> {
+ var pem = X509CertificateUtils.toPem(certificate);
+ request.addHeader("CERTIFICATE", StringUtilities.escape(pem));
+ });
+ return super.execute(request, responseHandler);
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java
new file mode 100644
index 00000000000..8112f5779e5
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java
@@ -0,0 +1,88 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.restapi;
+
+import com.yahoo.application.Networking;
+import com.yahoo.application.container.JDisc;
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.vespa.hosted.ca.restapi.mock.SecretStoreMock;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+import java.io.UncheckedIOException;
+import java.nio.charset.CharacterCodingException;
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * The superclass of REST API tests which require a functional container instance.
+ *
+ * @author mpolden
+ */
+public class ContainerTester {
+
+ private JDisc container;
+
+ @BeforeEach
+ public void startContainer() {
+ container = JDisc.fromServicesXml(servicesXml(), Networking.enable);
+ }
+
+ @AfterEach
+ public void stopContainer() {
+ container.close();
+ }
+
+ public SecretStoreMock secretStore() {
+ return (SecretStoreMock) container.components().getComponent(SecretStoreMock.class.getName());
+ }
+
+ public void assertResponse(int expectedStatus, String expectedBody, Request request) {
+ assertResponse(expectedStatus, (body) -> assertEquals(expectedBody, body), request);
+ }
+
+ public void assertResponse(int expectedStatus, Consumer<String> bodyAsserter, Request request) {
+ var response = container.handleRequest(request);
+ try {
+ bodyAsserter.accept(response.getBodyAsString());
+ } catch (CharacterCodingException e) {
+ throw new UncheckedIOException(e);
+ }
+ assertEquals(expectedStatus, response.getStatus());
+ assertEquals("application/json; charset=UTF-8", response.getHeaders().getFirst("Content-Type"));
+ }
+
+ private static String servicesXml() {
+ return "<container version='1.0'>\n" +
+ " <accesslog type=\"disabled\"/>\n" +
+ " <config name=\"container.handler.threadpool\">\n" +
+ " <maxthreads>10</maxthreads>\n" +
+ " </config>\n" +
+ " <config name='vespa.hosted.athenz.instanceproviderservice.config.athenz-provider-service'>\n" +
+ " <athenzCaTrustStore>/path/to/file</athenzCaTrustStore>\n" +
+ " <domain>vespa.external</domain>\n" +
+ " <serviceName>servicename</serviceName>\n" +
+ " <secretName>secretname</secretName>\n" +
+ " <secretVersion>0</secretVersion>\n" +
+ " <caCertSecretName>vespa.external.ca.cert</caCertSecretName>\n" +
+ " <certDnsSuffix>suffix</certDnsSuffix>\n" +
+ " <ztsUrl>https://localhost:123/</ztsUrl>\n" +
+ " </config>\n" +
+ " <component id='com.yahoo.vespa.hosted.ca.restapi.mock.SecretStoreMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.ca.restapi.mock.InstanceValidatorMock'/>\n" +
+ " <handler id='com.yahoo.vespa.hosted.ca.restapi.CertificateAuthorityApiHandler'>\n" +
+ " <binding>http://*/ca/v1/*</binding>\n" +
+ " </handler>\n" +
+ " <http>\n" +
+ " <server id='default' port='12345'/>\n" +
+ " <filtering>\n" +
+ " <request-chain id=\"my-default-chain\">\n" +
+ " <filter id='com.yahoo.vespa.hosted.ca.restapi.mock.PrincipalFromHeaderFilter' />\n" +
+ " <binding>http://*/*</binding>\n" +
+ " </request-chain>\n" +
+ " </filtering>\n" +
+ " </http>\n" +
+ "</container>";
+ }
+
+} \ No newline at end of file
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java
new file mode 100644
index 00000000000..ca624918beb
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java
@@ -0,0 +1,99 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.restapi;
+
+import com.yahoo.security.Pkcs10CsrUtils;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.slime.Slime;
+import com.yahoo.slime.SlimeUtils;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.identityprovider.api.ClusterType;
+import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper;
+import com.yahoo.vespa.athenz.identityprovider.api.IdentityType;
+import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
+import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
+import com.yahoo.vespa.hosted.ca.CertificateTester;
+import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity;
+import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh;
+import com.yahoo.vespa.hosted.ca.instance.InstanceRegistration;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author mpolden
+ */
+public class InstanceSerializerTest {
+
+ @Test
+ void deserialize_instance_registration() {
+ var csr = CertificateTester.createCsr();
+ var csrPem = Pkcs10CsrUtils.toPem(csr);
+ SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument(
+ "signature",
+ 0,
+ new VespaUniqueInstanceId(0, "cluster", "instance", "application", "tenant", "region", "prod", IdentityType.NODE),
+ new AthenzService("domain", "service"),
+ 0,
+ "configserverhostname",
+ "instancehostname",
+ Instant.now().truncatedTo(ChronoUnit.MICROS), // Truncate to the precision given from EntityBindingsMapper.toAttestationData()
+ Collections.emptySet(),
+ IdentityType.NODE,
+ ClusterType.CONTAINER);
+
+ var json = String.format("{\n" +
+ " \"provider\": \"provider_prod_us-north-1\",\n" +
+ " \"domain\": \"vespa.external\",\n" +
+ " \"service\": \"tenant\",\n" +
+ " \"attestationData\":\"%s\",\n" +
+ " \"csr\": \"" + csrPem + "\"\n" +
+ "}", StringUtilities.escape(EntityBindingsMapper.toAttestationData(signedIdentityDocument)));
+ var instanceRegistration = new InstanceRegistration("provider_prod_us-north-1", "vespa.external",
+ "tenant", signedIdentityDocument,
+ csr);
+ var deserialized = InstanceSerializer.registrationFromSlime(SlimeUtils.jsonToSlime(json));
+ assertEquals(instanceRegistration, deserialized);
+ }
+
+ @Test
+ void serialize_instance_identity() {
+ var certificate = CertificateTester.createCertificate();
+ var pem = X509CertificateUtils.toPem(certificate);
+ var identity = new InstanceIdentity("provider_prod_us-north-1", "tenant", "node1.example.com",
+ Optional.of(certificate));
+ var json = "{" +
+ "\"provider\":\"provider_prod_us-north-1\"," +
+ "\"service\":\"tenant\"," +
+ "\"instanceId\":\"node1.example.com\"," +
+ "\"x509Certificate\":\"" + pem.replace("\n", "\\n") + "\"" +
+ "}";
+ assertEquals(json, asJsonString(InstanceSerializer.identityToSlime(identity)));
+ }
+
+ @Test
+ void serialize_instance_refresh() {
+ var csr = CertificateTester.createCsr();
+ var csrPem = Pkcs10CsrUtils.toPem(csr);
+ var json = "{\"csr\": \"" + csrPem + "\"}";
+ var instanceRefresh = new InstanceRefresh(csr);
+ var deserialized = InstanceSerializer.refreshFromSlime(SlimeUtils.jsonToSlime(json));
+ assertEquals(instanceRefresh, deserialized);
+ }
+
+ private static String asJsonString(Slime slime) {
+ try {
+ return new String(SlimeUtils.toJsonBytes(slime), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java
new file mode 100644
index 00000000000..4151c1f15d7
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java
@@ -0,0 +1,27 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.restapi.mock;
+
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceConfirmation;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceValidator;
+
+/**
+ * @author mortent
+ */
+public class InstanceValidatorMock extends InstanceValidator {
+
+ public InstanceValidatorMock() {
+ super(null, null, null, null, null);
+ }
+
+ @Override
+ public boolean isValidInstance(InstanceConfirmation instanceConfirmation) {
+ return instanceConfirmation.attributes.get(SAN_DNS_ATTRNAME) != null &&
+ instanceConfirmation.attributes.get(SAN_IPS_ATTRNAME) != null;
+ }
+
+ @Override
+ public boolean isValidRefresh(InstanceConfirmation confirmation) {
+ return confirmation.attributes.get(SAN_DNS_ATTRNAME) != null &&
+ confirmation.attributes.get(SAN_IPS_ATTRNAME) != null;
+ }
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/PrincipalFromHeaderFilter.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/PrincipalFromHeaderFilter.java
new file mode 100644
index 00000000000..df98ba75dd2
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/PrincipalFromHeaderFilter.java
@@ -0,0 +1,34 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.restapi.mock;
+
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.filter.DiscFilterRequest;
+import com.yahoo.jdisc.http.filter.SecurityRequestFilter;
+import com.yahoo.jdisc.http.server.jetty.RequestUtils;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.athenz.api.AthenzPrincipal;
+import com.yahoo.vespa.athenz.api.AthenzService;
+
+import java.security.cert.X509Certificate;
+import java.util.Optional;
+
+/**
+ * Read principal from http header
+ *
+ * @author mortent
+ */
+public class PrincipalFromHeaderFilter implements SecurityRequestFilter {
+
+ @Override
+ public void filter(DiscFilterRequest request, ResponseHandler handler) {
+ String principal = request.getHeader("PRINCIPAL");
+ request.setUserPrincipal(new AthenzPrincipal(new AthenzService(principal)));
+
+ Optional<String> certificate = Optional.ofNullable(request.getHeader("CERTIFICATE"));
+ certificate.ifPresent(cert -> {
+ var x509cert = X509CertificateUtils.fromPem(StringUtilities.unescape(cert));
+ request.setAttribute(RequestUtils.JDISC_REQUEST_X509CERT, new X509Certificate[]{x509cert});
+ });
+ }
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/SecretStoreMock.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/SecretStoreMock.java
new file mode 100644
index 00000000000..5a9f4fd0b76
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/SecretStoreMock.java
@@ -0,0 +1,34 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.ca.restapi.mock;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.container.jdisc.secretstore.SecretStore;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author mpolden
+ */
+public class SecretStoreMock extends AbstractComponent implements SecretStore {
+
+ private final Map<String, String> secrets = new HashMap<>();
+
+ public SecretStoreMock setSecret(String key, String value) {
+ secrets.put(key, value);
+ return this;
+ }
+
+ @Override
+ public String getSecret(String key) {
+ if (!secrets.containsKey(key)) throw new RuntimeException("No such key '" + key + "'");
+ return secrets.get(key);
+ }
+
+ @Override
+ public String getSecret(String key, int version) {
+ if (!secrets.containsKey(key)) throw new RuntimeException("No such key '" + key + "'");
+ return secrets.get(key);
+ }
+
+}
diff --git a/configdefinitions/src/vespa/athenz-provider-service.def b/configdefinitions/src/vespa/athenz-provider-service.def
index 4c9c74f9b8f..2131aa88d30 100644
--- a/configdefinitions/src/vespa/athenz-provider-service.def
+++ b/configdefinitions/src/vespa/athenz-provider-service.def
@@ -13,11 +13,6 @@ secretName string
# Secret version
secretVersion int
-# Tempory resources
-sisSecretName string default=""
-sisSecretVersion int default=0
-sisUrl string default = ""
-
# Secret name of CA certificate
caCertSecretName string
diff --git a/configserver/CMakeLists.txt b/configserver/CMakeLists.txt
index 201d419c669..f189dc4f2c1 100644
--- a/configserver/CMakeLists.txt
+++ b/configserver/CMakeLists.txt
@@ -12,6 +12,7 @@ install(DIRECTORY DESTINATION conf/configserver)
install(DIRECTORY DESTINATION conf/configserver-app/components)
install(DIRECTORY DESTINATION conf/configserver-app/config-models)
+install_symlink(lib/jars/athenz-identity-provider-service-jar-with-dependencies.jar conf/configserver-app/components/athenz-identity-provider-service.jar)
install_symlink(lib/jars/config-model-fat.jar conf/configserver-app/components/config-model-fat.jar)
install_symlink(lib/jars/configserver-flags-jar-with-dependencies.jar conf/configserver-app/components/configserver-flags.jar)
install_symlink(lib/jars/flags-jar-with-dependencies.jar conf/configserver-app/components/flags.jar)
diff --git a/dist/vespa.spec b/dist/vespa.spec
index 0dee4e43910..58c2a18d3c1 100644
--- a/dist/vespa.spec
+++ b/dist/vespa.spec
@@ -586,6 +586,7 @@ fi
%{_prefix}/include
%dir %{_prefix}/lib
%dir %{_prefix}/lib/jars
+%{_prefix}/lib/jars/athenz-identity-provider-service-jar-with-dependencies.jar
%{_prefix}/lib/jars/cloud-tenant-cd-jar-with-dependencies.jar
%{_prefix}/lib/jars/clustercontroller-apps-jar-with-dependencies.jar
%{_prefix}/lib/jars/clustercontroller-core-jar-with-dependencies.jar
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
index c3099c48014..f4584f564ad 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
@@ -346,13 +346,6 @@ public class Flags {
"Takes effect at redeployment",
APPLICATION_ID);
- public static final UnboundBooleanFlag VESPA_ATHENZ_PROVIDER = defineFeatureFlag(
- "vespa-athenz-provider", false,
- List.of("mortent"), "2023-02-22", "2023-05-01",
- "Enable athenz provider in public systems",
- "Takes effect on next config server container start",
- ZONE_ID);
-
/** WARNING: public for testing: All flags should be defined in {@link Flags}. */
public static UnboundBooleanFlag defineFeatureFlag(String flagId, boolean defaultValue, List<String> owners,
String createdAt, String expiresAt, String description,
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java
index 6bd7d98e207..fc49dcc744c 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java
@@ -41,7 +41,6 @@ import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
-import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
@@ -190,9 +189,11 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer {
Pkcs10Csr csr = csrGenerator.generateInstanceCsr(
context.identity(), doc.providerUniqueId(), doc.ipAddresses(), doc.clusterType(), keyPair);
- // Allow all zts hosts while removing SIS
- HostnameVerifier ztsHostNameVerifier = (hostname, sslSession) -> true;
- try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint(doc)).withIdentityProvider(hostIdentityProvider).withHostnameVerifier(ztsHostNameVerifier).build()) {
+ // Set up a hostname verified for zts if this is configured to use the config server (internal zts) apis
+ HostnameVerifier ztsHostNameVerifier = useInternalZts
+ ? new AthenzIdentityVerifier(Set.of(configserverIdentity))
+ : null;
+ try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint).withIdentityProvider(hostIdentityProvider).withHostnameVerifier(ztsHostNameVerifier).build()) {
InstanceIdentity instanceIdentity =
ztsClient.registerInstance(
configserverIdentity,
@@ -205,15 +206,6 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer {
}
}
- /**
- * Return zts url from identity document, fallback to ztsEndpoint
- */
- private URI ztsEndpoint(SignedIdentityDocument doc) {
- return Optional.ofNullable(doc.ztsUrl())
- .filter(s -> !s.isBlank())
- .map(URI::create)
- .orElse(ztsEndpoint);
- }
private void refreshIdentity(NodeAgentContext context, ContainerPath privateKeyFile, ContainerPath certificateFile,
ContainerPath identityDocumentFile, SignedIdentityDocument doc) {
KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA);
@@ -225,9 +217,11 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer {
.build();
try {
- // Allow all zts hosts while removing SIS
- HostnameVerifier ztsHostNameVerifier = (hostname, sslSession) -> true;
- try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint(doc)).withSslContext(containerIdentitySslContext).withHostnameVerifier(ztsHostNameVerifier).build()) {
+ // Set up a hostname verified for zts if this is configured to use the config server (internal zts) apis
+ HostnameVerifier ztsHostNameVerifier = useInternalZts
+ ? new AthenzIdentityVerifier(Set.of(configserverIdentity))
+ : null;
+ try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint).withSslContext(containerIdentitySslContext).withHostnameVerifier(ztsHostNameVerifier).build()) {
InstanceIdentity instanceIdentity =
ztsClient.refreshInstance(
configserverIdentity,
diff --git a/pom.xml b/pom.xml
index c4b6e200e2e..e1a7f8dc900 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,6 +27,7 @@
<module>airlift-zstd</module>
<module>application</module>
<module>application-model</module>
+ <module>athenz-identity-provider-service</module>
<module>bundle-plugin-test</module>
<module>client</module>
<module>cloud-tenant-base</module>
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java
index 2d77d2ceda1..9b7b666e353 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java
@@ -4,10 +4,8 @@ package com.yahoo.vespa.athenz.identityprovider.api;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.AthenzService;
import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity;
-import com.yahoo.vespa.athenz.utils.AthenzIdentities;
import java.io.IOException;
import java.io.InputStream;
@@ -60,8 +58,6 @@ public class EntityBindingsMapper {
entity.ipAddresses(),
IdentityType.fromId(entity.identityType()),
Optional.ofNullable(entity.clusterType()).map(ClusterType::from).orElse(null),
- entity.ztsUrl(),
- Optional.ofNullable(entity.serviceIdentity()).map(AthenzIdentities::from).orElse(null),
entity.unknownAttributes());
}
@@ -78,8 +74,6 @@ public class EntityBindingsMapper {
model.ipAddresses(),
model.identityType().id(),
Optional.ofNullable(model.clusterType()).map(ClusterType::toConfigValue).orElse(null),
- model.ztsUrl(),
- Optional.ofNullable(model.serviceIdentity()).map(AthenzIdentity::getFullName).orElse(null),
model.unknownAttributes());
}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java
index ac37bb3368e..49a39d25e87 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java
@@ -1,10 +1,8 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.athenz.identityprovider.api;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.AthenzService;
-import java.net.URL;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@@ -19,8 +17,7 @@ import java.util.Set;
public record SignedIdentityDocument(String signature, int signingKeyVersion, VespaUniqueInstanceId providerUniqueId,
AthenzService providerService, int documentVersion, String configServerHostname,
String instanceHostname, Instant createdAt, Set<String> ipAddresses,
- IdentityType identityType, ClusterType clusterType, String ztsUrl,
- AthenzIdentity serviceIdentity, Map<String, Object> unknownAttributes) {
+ IdentityType identityType, ClusterType clusterType, Map<String, Object> unknownAttributes) {
public SignedIdentityDocument {
ipAddresses = Set.copyOf(ipAddresses);
@@ -36,12 +33,12 @@ public record SignedIdentityDocument(String signature, int signingKeyVersion, Ve
public SignedIdentityDocument(String signature, int signingKeyVersion, VespaUniqueInstanceId providerUniqueId,
AthenzService providerService, int documentVersion, String configServerHostname,
String instanceHostname, Instant createdAt, Set<String> ipAddresses,
- IdentityType identityType, ClusterType clusterType, String ztsUrl, AthenzIdentity serviceIdentity) {
+ IdentityType identityType, ClusterType clusterType) {
this(signature, signingKeyVersion, providerUniqueId, providerService, documentVersion, configServerHostname,
- instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl, serviceIdentity, Map.of());
+ instanceHostname, createdAt, ipAddresses, identityType, clusterType, Map.of());
}
- public static final int DEFAULT_DOCUMENT_VERSION = 3;
+ public static final int DEFAULT_DOCUMENT_VERSION = 2;
public boolean outdated() { return documentVersion < DEFAULT_DOCUMENT_VERSION; }
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java
index fc0dff3b97b..c37dd2f9147 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java
@@ -4,7 +4,6 @@ package com.yahoo.vespa.athenz.identityprovider.api.bindings;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;
@@ -15,11 +14,10 @@ import java.util.Set;
/**
* @author bjorncs
*/
-@JsonInclude(JsonInclude.Include.NON_NULL)
public record SignedIdentityDocumentEntity(
String signature, int signingKeyVersion, String providerUniqueId, String providerService, int documentVersion,
String configServerHostname, String instanceHostname, Instant createdAt, Set<String> ipAddresses,
- String identityType, String clusterType, String ztsUrl, String serviceIdentity, Map<String, Object> unknownAttributes) {
+ String identityType, String clusterType, Map<String, Object> unknownAttributes) {
@JsonCreator
public SignedIdentityDocumentEntity(@JsonProperty("signature") String signature,
@@ -32,11 +30,9 @@ public record SignedIdentityDocumentEntity(
@JsonProperty("created-at") Instant createdAt,
@JsonProperty("ip-addresses") Set<String> ipAddresses,
@JsonProperty("identity-type") String identityType,
- @JsonProperty("cluster-type") String clusterType,
- @JsonProperty("zts-url") String ztsUrl,
- @JsonProperty("service-identity") String serviceIdentity) {
+ @JsonProperty("cluster-type") String clusterType) {
this(signature, signingKeyVersion, providerUniqueId, providerService, documentVersion, configServerHostname,
- instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl, serviceIdentity, new HashMap<>());
+ instanceHostname, createdAt, ipAddresses, identityType, clusterType, new HashMap<>());
}
@JsonProperty("signature") @Override public String signature() { return signature; }
@@ -50,8 +46,6 @@ public record SignedIdentityDocumentEntity(
@JsonProperty("ip-addresses") @Override public Set<String> ipAddresses() { return ipAddresses; }
@JsonProperty("identity-type") @Override public String identityType() { return identityType; }
@JsonProperty("cluster-type") @Override public String clusterType() { return clusterType; }
- @JsonProperty("zts-url") @Override public String ztsUrl() { return ztsUrl; }
- @JsonProperty("service-identity") @Override public String serviceIdentity() { return serviceIdentity; }
@JsonAnyGetter @Override public Map<String, Object> unknownAttributes() { return unknownAttributes; }
@JsonAnySetter public void set(String name, Object value) { unknownAttributes.put(name, value); }
}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSigner.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSigner.java
index 019f73fc6bf..14d06fe83f2 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSigner.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSigner.java
@@ -2,7 +2,6 @@
package com.yahoo.vespa.athenz.identityprovider.client;
import com.yahoo.security.SignatureUtils;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.AthenzService;
import com.yahoo.vespa.athenz.identityprovider.api.IdentityType;
import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
@@ -19,7 +18,6 @@ import java.util.Base64;
import java.util.Set;
import java.util.TreeSet;
-import static com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
@@ -37,15 +35,13 @@ public class IdentityDocumentSigner {
Instant createdAt,
Set<String> ipAddresses,
IdentityType identityType,
- PrivateKey privateKey,
- AthenzIdentity serviceIdentity) {
+ PrivateKey privateKey) {
try {
Signature signer = SignatureUtils.createSigner(privateKey);
signer.initSign(privateKey);
writeToSigner(
signer, providerUniqueId, providerService, configServerHostname, instanceHostname, createdAt,
ipAddresses, identityType);
- writeToSigner(signer, serviceIdentity);
byte[] signature = signer.sign();
return Base64.getEncoder().encodeToString(signature);
} catch (GeneralSecurityException e) {
@@ -60,9 +56,6 @@ public class IdentityDocumentSigner {
writeToSigner(
signer, doc.providerUniqueId(), doc.providerService(), doc.configServerHostname(),
doc.instanceHostname(), doc.createdAt(), doc.ipAddresses(), doc.identityType());
- if (doc.documentVersion() >= DEFAULT_DOCUMENT_VERSION) {
- writeToSigner(signer, doc.serviceIdentity());
- }
return signer.verify(Base64.getDecoder().decode(doc.signature()));
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
@@ -89,8 +82,4 @@ public class IdentityDocumentSigner {
}
signer.update(identityType.id().getBytes(UTF_8));
}
-
- private static void writeToSigner(Signature signer, AthenzIdentity serviceIdentity) throws SignatureException{
- signer.update(serviceIdentity.getFullName().getBytes(UTF_8));
- }
}
diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapperTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapperTest.java
index 2a68f6fd231..f8c119190a6 100644
--- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapperTest.java
+++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapperTest.java
@@ -30,7 +30,6 @@ class EntityBindingsMapperTest {
"ip-addresses": [],
"identity-type": "node",
"cluster-type": "admin",
- "zts-url": "https://zts.url/",
"unknown-string": "string-value",
"unknown-object": { "member-in-unknown-object": 123 }
}
diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSignerTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSignerTest.java
index ff85cb79f02..0b8ff4277f1 100644
--- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSignerTest.java
+++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSignerTest.java
@@ -3,13 +3,11 @@ package com.yahoo.vespa.athenz.identityprovider.client;
import com.yahoo.security.KeyAlgorithm;
import com.yahoo.security.KeyUtils;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.AthenzService;
import com.yahoo.vespa.athenz.identityprovider.api.ClusterType;
import com.yahoo.vespa.athenz.identityprovider.api.IdentityType;
import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
-import com.yahoo.vespa.athenz.utils.AthenzIdentities;
import org.junit.jupiter.api.Test;
import java.security.KeyPair;
@@ -38,54 +36,37 @@ public class IdentityDocumentSignerTest {
private static final Instant createdAt = Instant.EPOCH;
private static final HashSet<String> ipAddresses = new HashSet<>(Arrays.asList("1.2.3.4", "::1"));
private static final ClusterType clusterType = ClusterType.CONTAINER;
- private static final String ztsUrl = "https://foo";
- private static final AthenzIdentity serviceIdentity = new AthenzService("vespa", "node");
@Test
void generates_and_validates_signature() {
IdentityDocumentSigner signer = new IdentityDocumentSigner();
String signature =
signer.generateSignature(id, providerService, configserverHostname, instanceHostname, createdAt,
- ipAddresses, identityType, keyPair.getPrivate(), serviceIdentity);
+ ipAddresses, identityType, keyPair.getPrivate());
SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument(
signature, KEY_VERSION, id, providerService, DEFAULT_DOCUMENT_VERSION, configserverHostname,
- instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl, serviceIdentity);
+ instanceHostname, createdAt, ipAddresses, identityType, clusterType);
assertTrue(signer.hasValidSignature(signedIdentityDocument, keyPair.getPublic()));
}
@Test
- void ignores_cluster_type_and_zts_url() {
+ void ignores_cluster_type() {
IdentityDocumentSigner signer = new IdentityDocumentSigner();
String signature =
signer.generateSignature(id, providerService, configserverHostname, instanceHostname, createdAt,
- ipAddresses, identityType, keyPair.getPrivate(), serviceIdentity);
+ ipAddresses, identityType, keyPair.getPrivate());
- var docWithoutIgnoredFields = new SignedIdentityDocument(
+ var docWithoutClusterType = new SignedIdentityDocument(
signature, KEY_VERSION, id, providerService, DEFAULT_DOCUMENT_VERSION, configserverHostname,
- instanceHostname, createdAt, ipAddresses, identityType, null, null, serviceIdentity);
- var docWithIgnoredFields = new SignedIdentityDocument(
+ instanceHostname, createdAt, ipAddresses, identityType, null);
+ var docWithClusterType = new SignedIdentityDocument(
signature, KEY_VERSION, id, providerService, DEFAULT_DOCUMENT_VERSION, configserverHostname,
- instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl, serviceIdentity);
-
- assertTrue(signer.hasValidSignature(docWithoutIgnoredFields, keyPair.getPublic()));
- assertEquals(docWithIgnoredFields.signature(), docWithoutIgnoredFields.signature());
- }
-
- @Test
- void validates_signature_for_new_and_old_versions() {
- IdentityDocumentSigner signer = new IdentityDocumentSigner();
- String signature =
- signer.generateSignature(id, providerService, configserverHostname, instanceHostname, createdAt,
- ipAddresses, identityType, keyPair.getPrivate(), serviceIdentity);
-
- SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument(
- signature, KEY_VERSION, id, providerService, DEFAULT_DOCUMENT_VERSION, configserverHostname,
- instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl, serviceIdentity);
-
- assertTrue(signer.hasValidSignature(signedIdentityDocument, keyPair.getPublic()));
+ instanceHostname, createdAt, ipAddresses, identityType, clusterType);
+ assertTrue(signer.hasValidSignature(docWithoutClusterType, keyPair.getPublic()));
+ assertEquals(docWithClusterType.signature(), docWithoutClusterType.signature());
}
} \ No newline at end of file
diff --git a/vespa-dependencies-enforcer/allowed-maven-dependencies.txt b/vespa-dependencies-enforcer/allowed-maven-dependencies.txt
index a3a488d1e25..d5166a7e22f 100644
--- a/vespa-dependencies-enforcer/allowed-maven-dependencies.txt
+++ b/vespa-dependencies-enforcer/allowed-maven-dependencies.txt
@@ -28,6 +28,7 @@ com.google.code.findbugs:jsr305:3.0.2
com.google.errorprone:error_prone_annotations:2.18.0
com.google.guava:failureaccess:1.0.1
com.google.guava:guava:27.1-jre
+com.google.inject:guice:4.2.3
com.google.inject:guice:4.2.3:no_aop
com.google.j2objc:j2objc-annotations:1.1
com.google.protobuf:protobuf-java:3.21.7
@@ -220,7 +221,6 @@ xml-apis:xml-apis:1.4.01
com.github.luben:zstd-jni:1.5.2-1
com.github.tomakehurst:wiremock-jre8-standalone:2.35.0
com.google.guava:guava-testlib:27.1-jre
-com.google.inject:guice:4.2.3
com.google.jimfs:jimfs:1.2
junit:junit:4.13.2
net.bytebuddy:byte-buddy:1.11.19