diff options
author | Bjørn Christian Seime <bjorn.christian@seime.no> | 2018-06-15 14:56:10 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-06-15 14:56:10 +0200 |
commit | f966facc1dace916b3cddb564c9b41877bd64176 (patch) | |
tree | 1bc0585fa6b5b719fd4a7d05aa72a1662ea1490e | |
parent | 712c85caee3720d81e095528a2ba86355b9589b2 (diff) | |
parent | 75aa726659d9b027287d3bd12e5285c99f90d7cb (diff) |
Merge pull request #6206 from vespa-engine/bjorncs/zts-client
Bjorncs/zts client
21 files changed, 386 insertions, 154 deletions
diff --git a/athenz-identity-provider-service/pom.xml b/athenz-identity-provider-service/pom.xml index 982cb89f2bf..16e18e99e1b 100644 --- a/athenz-identity-provider-service/pom.xml +++ b/athenz-identity-provider-service/pom.xml @@ -63,38 +63,6 @@ <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> </dependency> - <dependency> - <groupId>com.yahoo.athenz</groupId> - <artifactId>athenz-zts-java-client</artifactId> - <scope>compile</scope> - <exclusions> - <!--Exclude all bundles provided by JDisc --> - <exclusion> - <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> - </exclusion> - <exclusion> - <groupId>org.bouncycastle</groupId> - <artifactId>bcprov-jdk15on</artifactId> - </exclusion> - <exclusion> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - </exclusion> - <exclusion> - <groupId>com.fasterxml.jackson.core</groupId> - <artifactId>jackson-core</artifactId> - </exclusion> - <exclusion> - <groupId>com.fasterxml.jackson.core</groupId> - <artifactId>jackson-databind</artifactId> - </exclusion> - <exclusion> - <groupId>com.fasterxml.jackson.core</groupId> - <artifactId>jackson-annotations</artifactId> - </exclusion> - </exclusions> - </dependency> <!-- TEST --> <dependency> diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java index 2e0cf04ae4f..2fc696d722b 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java @@ -9,22 +9,34 @@ import com.yahoo.jdisc.http.ssl.SslKeyStoreConfigurator; import com.yahoo.jdisc.http.ssl.SslKeyStoreContext; import com.yahoo.log.LogLevel; 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.tls.KeyStoreBuilder; import com.yahoo.vespa.athenz.tls.KeyStoreType; +import com.yahoo.vespa.athenz.tls.KeyUtils; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; import com.yahoo.vespa.athenz.utils.SiaUtils; import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.AthenzCertificateClient; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.KeyPair; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.Executors; @@ -46,9 +58,10 @@ public class AthenzSslKeyStoreConfigurator extends AbstractComponent implements 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 Path CA_CERT_FILE = VESPA_SIA_DIRECTORY.resolve("ca-certs.pem"); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - private final AthenzCertificateClient certificateClient; + private final ZtsClient ztsClient; private final KeyProvider keyProvider; private final AthenzProviderServiceConfig.Zones zoneConfig; private final Duration updatePeriod; @@ -63,40 +76,50 @@ public class AthenzSslKeyStoreConfigurator extends AbstractComponent implements ConfigserverConfig configserverConfig) { AthenzProviderServiceConfig.Zones zoneConfig = getZoneConfig(config, zone); AthenzService configserverIdentity = new AthenzService(zoneConfig.domain(), zoneConfig.serviceName()); - AthenzCertificateClient certificateClient = new AthenzCertificateClient(bootstrapIdentity, zoneConfig); Duration updatePeriod = Duration.ofDays(config.updatePeriodDays()); - this.certificateClient = certificateClient; + DefaultZtsClient ztsClient = new DefaultZtsClient(URI.create(zoneConfig.ztsUrl()).resolve("/zts/v1"), bootstrapIdentity); // TODO Remove URI.resolve() once config in hosted is updated + this.ztsClient = ztsClient; this.keyProvider = keyProvider; this.zoneConfig = zoneConfig; - this.currentKeyStore = initializeKeystore(configserverIdentity, keyProvider, certificateClient, zoneConfig, updatePeriod); + this.currentKeyStore = initializeKeystore(configserverIdentity, keyProvider, ztsClient, zoneConfig, updatePeriod); this.updatePeriod = updatePeriod; this.configserverIdentity = configserverIdentity; } private static KeyStoreAndPassword initializeKeystore(AthenzService configserverIdentity, KeyProvider keyProvider, - AthenzCertificateClient zoneConfig, + ZtsClient ztsClient, AthenzProviderServiceConfig.Zones keystoreCacheDirectory, Duration updatePeriod) { return tryReadKeystoreFile(configserverIdentity, updatePeriod) - .orElseGet(() -> downloadCertificate(configserverIdentity, keyProvider, zoneConfig, keystoreCacheDirectory)); + .orElseGet(() -> downloadCertificate(configserverIdentity, keyProvider, ztsClient, keystoreCacheDirectory)); } private static Optional<KeyStoreAndPassword> 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(); - - char[] password = generateKeystorePassword(); - KeyStore keyStore = KeyStoreBuilder.withType(KeyStoreType.JKS) - .withKeyEntry(CERTIFICATE_ALIAS, privateKey.get(), password, certificate.get()) - .build(); - return Optional.of(new KeyStoreAndPassword(keyStore, password)); + try { + 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(); + if (Files.notExists(CA_CERT_FILE)) return Optional.empty(); + List<X509Certificate> caCertificates = X509CertificateUtils.certificateListFromPem(new String(Files.readAllBytes(CA_CERT_FILE))); + + List<X509Certificate> chain = new ArrayList<>(); + chain.add(certificate.get()); + chain.addAll(caCertificates); + + char[] password = generateKeystorePassword(); + KeyStore keyStore = KeyStoreBuilder.withType(KeyStoreType.JKS) + .withKeyEntry(CERTIFICATE_ALIAS, privateKey.get(), password, chain) + .build(); + return Optional.of(new KeyStoreAndPassword(keyStore, password)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } @Override @@ -113,6 +136,7 @@ public class AthenzSslKeyStoreConfigurator extends AbstractComponent implements try { scheduler.shutdownNow(); scheduler.awaitTermination(30, TimeUnit.SECONDS); + ztsClient.close(); } catch (InterruptedException e) { throw new RuntimeException("Failed to shutdown Athenz certificate updater on time", e); } @@ -129,27 +153,41 @@ public class AthenzSslKeyStoreConfigurator extends AbstractComponent implements private static KeyStoreAndPassword downloadCertificate(AthenzService configserverIdentity, KeyProvider keyProvider, - AthenzCertificateClient certificateClient, + ZtsClient ztsClient, AthenzProviderServiceConfig.Zones zoneConfig) { PrivateKey privateKey = keyProvider.getPrivateKey(zoneConfig.secretVersion()); - X509Certificate certificate = certificateClient.updateCertificate(privateKey); - writeCredentials(configserverIdentity, certificate, privateKey); + PublicKey publicKey = KeyUtils.extractPublicKey(privateKey); + Identity serviceIdentity = ztsClient.getServiceIdentity(configserverIdentity, + Integer.toString(zoneConfig.secretVersion()), + new KeyPair(publicKey, privateKey), + zoneConfig.certDnsSuffix()); + X509Certificate certificate = serviceIdentity.certificate(); + writeCredentials(configserverIdentity, certificate, serviceIdentity.caCertificates(), privateKey); Instant expirationTime = certificate.getNotAfter().toInstant(); Duration expiry = Duration.between(certificate.getNotBefore().toInstant(), expirationTime); log.log(LogLevel.INFO, String.format("Got Athenz x509 certificate with expiry %s (expires %s)", expiry, expirationTime)); + List<X509Certificate> chain = new ArrayList<>(); + chain.add(certificate); + chain.addAll(serviceIdentity.caCertificates()); char[] keystorePassword = generateKeystorePassword(); KeyStore keyStore = KeyStoreBuilder.withType(KeyStoreType.JKS) - .withKeyEntry(CERTIFICATE_ALIAS, privateKey, keystorePassword, certificate) + .withKeyEntry(CERTIFICATE_ALIAS, privateKey, keystorePassword, chain) .build(); return new KeyStoreAndPassword(keyStore, keystorePassword); } private static void writeCredentials(AthenzService configserverIdentity, X509Certificate certificate, + List<X509Certificate> caCertificates, PrivateKey privateKey) { SiaUtils.writeCertificateFile(VESPA_SIA_DIRECTORY, configserverIdentity, certificate); SiaUtils.writePrivateKeyFile(VESPA_SIA_DIRECTORY, configserverIdentity, privateKey); + try { + Files.write(CA_CERT_FILE, X509CertificateUtils.toPem(caCertificates).getBytes()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } private static char[] generateKeystorePassword() { @@ -168,7 +206,7 @@ public class AthenzSslKeyStoreConfigurator extends AbstractComponent implements public void run() { try { log.log(LogLevel.INFO, "Updating Athenz certificate from ZTS"); - currentKeyStore = downloadCertificate(configserverIdentity, keyProvider, certificateClient, zoneConfig); + currentKeyStore = downloadCertificate(configserverIdentity, keyProvider, ztsClient, zoneConfig); sslKeyStoreContext.updateKeyStore(currentKeyStore.keyStore, new String(currentKeyStore.password)); log.log(LogLevel.INFO, "Athenz certificate reload successfully completed"); } catch (Throwable e) { diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java deleted file mode 100644 index 193a573c98d..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; - -import com.yahoo.athenz.zts.InstanceRefreshRequest; -import com.yahoo.athenz.zts.ZTSClient; -import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; -import com.yahoo.vespa.athenz.tls.X509CertificateUtils; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; - -import javax.net.ssl.SSLContext; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; - -/** - * @author bjorncs - */ -public class AthenzCertificateClient { - - private final AthenzProviderServiceConfig.Zones zoneConfig; - private final ServiceIdentityProvider bootstrapIdentity; - - public AthenzCertificateClient(ServiceIdentityProvider bootstrapIdentity, - AthenzProviderServiceConfig.Zones zoneConfig) { - this.bootstrapIdentity = bootstrapIdentity; - this.zoneConfig = zoneConfig; - } - - public X509Certificate updateCertificate(PrivateKey privateKey) { - SSLContext bootstrapSslContext = bootstrapIdentity.getIdentitySslContext(); - ZTSClient ztsClient = new ZTSClient(zoneConfig.ztsUrl(), bootstrapSslContext); - InstanceRefreshRequest req = - ZTSClient.generateInstanceRefreshRequest( - zoneConfig.domain(), zoneConfig.serviceName(), privateKey, zoneConfig.certDnsSuffix(), /*expiryTime*/0); - req.setKeyId(Integer.toString(zoneConfig.secretVersion())); - String pemEncoded = ztsClient.postInstanceRefreshRequest(zoneConfig.domain(), zoneConfig.serviceName(), req) - .getCertificate(); - return X509CertificateUtils.fromPem(pemEncoded); - } - -} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/DefaultZtsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/DefaultZtsClient.java index dd97b20055d..f8654bbaa68 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/DefaultZtsClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/DefaultZtsClient.java @@ -10,12 +10,15 @@ import com.yahoo.vespa.athenz.api.AthenzRole; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.api.NToken; import com.yahoo.vespa.athenz.api.ZToken; +import com.yahoo.vespa.athenz.client.zts.bindings.IdentityResponseEntity; import com.yahoo.vespa.athenz.client.zts.bindings.InstanceIdentityCredentials; import com.yahoo.vespa.athenz.client.zts.bindings.InstanceRefreshInformation; +import com.yahoo.vespa.athenz.client.zts.bindings.IdentityRefreshRequestEntity; import com.yahoo.vespa.athenz.client.zts.bindings.InstanceRegisterInformation; import com.yahoo.vespa.athenz.client.zts.bindings.RoleCertificateRequestEntity; import com.yahoo.vespa.athenz.client.zts.bindings.RoleCertificateResponseEntity; import com.yahoo.vespa.athenz.client.zts.bindings.RoleTokenResponseEntity; +import com.yahoo.vespa.athenz.client.zts.utils.IdentityCsrGenerator; import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; import com.yahoo.vespa.athenz.tls.Pkcs10Csr; import com.yahoo.vespa.athenz.tls.Pkcs10CsrBuilder; @@ -127,6 +130,27 @@ public class DefaultZtsClient implements ZtsClient { } @Override + public Identity getServiceIdentity(AthenzService identity, String keyId, Pkcs10Csr csr) { + URI uri = ztsUrl.resolve(String.format("instance/%s/%s/refresh", identity.getDomainName(), identity.getName())); + HttpUriRequest request = RequestBuilder.post() + .setUri(uri) + .setEntity(toJsonStringEntity(new IdentityRefreshRequestEntity(csr, keyId))) + .build(); + return withClient(client -> { + try (CloseableHttpResponse response = client.execute(request)) { + IdentityResponseEntity entity = readEntity(response, IdentityResponseEntity.class); + return new Identity(entity.certificate(), entity.caCertificateBundle()); + } + }); + } + + @Override + public Identity getServiceIdentity(AthenzService identity, String keyId, KeyPair keyPair, String dnsSuffix) { + Pkcs10Csr csr = new IdentityCsrGenerator(dnsSuffix).generateIdentityCsr(identity, keyPair); + return getServiceIdentity(identity, keyId, csr); + } + + @Override public ZToken getRoleToken(AthenzDomain domain) { return getRoleToken(domain, null); } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/Identity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/Identity.java new file mode 100644 index 00000000000..455f3c06d1d --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/Identity.java @@ -0,0 +1,29 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zts; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * The identity of a service + * + * @author bjorncs + */ +public class Identity { + + private final X509Certificate certificate; + private final List<X509Certificate> caCertificates; + + public Identity(X509Certificate certificate, List<X509Certificate> caCertificates) { + this.certificate = certificate; + this.caCertificates = caCertificates; + } + + public X509Certificate certificate() { + return certificate; + } + + public List<X509Certificate> caCertificates() { + return caCertificates; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/ZtsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/ZtsClient.java index 87a204095d9..9502deca1c0 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/ZtsClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/ZtsClient.java @@ -43,6 +43,25 @@ public interface ZtsClient extends AutoCloseable { Pkcs10Csr csr); /** + * Get service identity + * + * @return A x509 certificate with CA certificates + */ + Identity getServiceIdentity(AthenzService identity, + String keyId, + Pkcs10Csr csr); + + /** + * Get service identity + * + * @return A x509 certificate with CA certificates + */ + Identity getServiceIdentity(AthenzService identity, + String keyId, + KeyPair keyPair, + String dnsSuffix); + + /** * Fetch a role token for the target domain * * @param domain Target domain diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/IdentityRefreshRequestEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/IdentityRefreshRequestEntity.java new file mode 100644 index 00000000000..47ae9cd2d3f --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/IdentityRefreshRequestEntity.java @@ -0,0 +1,24 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zts.bindings; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.yahoo.vespa.athenz.client.zts.bindings.serializers.Pkcs10CsrSerializer; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; + +/** + * @author bjorncs + */ +public class IdentityRefreshRequestEntity { + + @JsonProperty("csr") @JsonSerialize(using = Pkcs10CsrSerializer.class) + private final Pkcs10Csr csr; + + @JsonProperty("keyId") + private final String keyId; + + public IdentityRefreshRequestEntity(Pkcs10Csr csr, String keyId) { + this.csr = csr; + this.keyId = keyId; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/IdentityResponseEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/IdentityResponseEntity.java new file mode 100644 index 00000000000..7bd04362599 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/IdentityResponseEntity.java @@ -0,0 +1,40 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zts.bindings; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.yahoo.vespa.athenz.client.zts.bindings.serializers.X509CertificateDeserializer; +import com.yahoo.vespa.athenz.client.zts.bindings.serializers.X509CertificateListDeserializer; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Identity response entity + * + * @author bjorncs + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class IdentityResponseEntity { + + private final X509Certificate certificate; + private final List<X509Certificate> caCertificateBundle; + + @JsonCreator + public IdentityResponseEntity( + @JsonProperty("certificate") @JsonDeserialize(using = X509CertificateDeserializer.class) X509Certificate certificate, + @JsonProperty("caCertBundle") @JsonDeserialize(using = X509CertificateListDeserializer.class) List<X509Certificate> caCertificateBundle) { + this.certificate = certificate; + this.caCertificateBundle = caCertificateBundle; + } + + public X509Certificate certificate() { + return certificate; + } + + public List<X509Certificate> caCertificateBundle() { + return caCertificateBundle; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/InstanceIdentityCredentials.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/InstanceIdentityCredentials.java index 5c265f14813..0ab697a1c4c 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/InstanceIdentityCredentials.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/InstanceIdentityCredentials.java @@ -4,13 +4,9 @@ package com.yahoo.vespa.athenz.client.zts.bindings; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.yahoo.vespa.athenz.tls.X509CertificateUtils; +import com.yahoo.vespa.athenz.client.zts.bindings.serializers.X509CertificateDeserializer; -import java.io.IOException; import java.security.cert.X509Certificate; /** @@ -39,11 +35,4 @@ public class InstanceIdentityCredentials { return serviceToken; } - public static class X509CertificateDeserializer extends JsonDeserializer<X509Certificate> { - @Override - public X509Certificate deserialize(JsonParser parser, DeserializationContext context) throws IOException { - return X509CertificateUtils.fromPem(parser.getValueAsString()); - } - } - } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/InstanceRefreshInformation.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/InstanceRefreshInformation.java index 6c956ddb410..b842ef43500 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/InstanceRefreshInformation.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/InstanceRefreshInformation.java @@ -4,8 +4,9 @@ package com.yahoo.vespa.athenz.client.zts.bindings; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.yahoo.vespa.athenz.client.zts.bindings.serializers.Pkcs10CsrSerializer; import com.yahoo.vespa.athenz.tls.Pkcs10Csr; -import com.yahoo.vespa.athenz.tls.Pkcs10CsrUtils; /** * @author bjorncs @@ -15,13 +16,14 @@ import com.yahoo.vespa.athenz.tls.Pkcs10CsrUtils; public class InstanceRefreshInformation { @JsonProperty("csr") - private final String csr; + @JsonSerialize(using = Pkcs10CsrSerializer.class) + private final Pkcs10Csr csr; @JsonProperty("token") private final boolean requestServiceToken; public InstanceRefreshInformation(Pkcs10Csr csr, boolean requestServiceToken) { - this.csr = Pkcs10CsrUtils.toPem(csr); + this.csr = csr; this.requestServiceToken = requestServiceToken; } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/RoleCertificateRequestEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/RoleCertificateRequestEntity.java index 9c56e5a60d6..f329ffbbd2c 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/RoleCertificateRequestEntity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/RoleCertificateRequestEntity.java @@ -8,8 +8,8 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.yahoo.vespa.athenz.client.zts.bindings.serializers.Pkcs10CsrSerializer; import com.yahoo.vespa.athenz.tls.Pkcs10Csr; -import com.yahoo.vespa.athenz.tls.Pkcs10CsrUtils; import java.io.IOException; import java.time.Duration; @@ -20,7 +20,7 @@ import java.time.Duration; @JsonIgnoreProperties(ignoreUnknown = true) public class RoleCertificateRequestEntity { @JsonProperty("csr") - @JsonSerialize(using = CsrSerializer.class) + @JsonSerialize(using = Pkcs10CsrSerializer.class) public final Pkcs10Csr csr; @JsonProperty("expiryTime") @@ -33,15 +33,6 @@ public class RoleCertificateRequestEntity { this.expiryTime = expiryTime; } - public static class CsrSerializer extends JsonSerializer<Pkcs10Csr> { - @Override - public void serialize(Pkcs10Csr csr, - JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(Pkcs10CsrUtils.toPem(csr)); - } - } - public static class ExpirySerializer extends JsonSerializer<Duration> { @Override public void serialize(Duration duration, diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/RoleCertificateResponseEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/RoleCertificateResponseEntity.java index 1b4bd463392..e80f5626843 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/RoleCertificateResponseEntity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/RoleCertificateResponseEntity.java @@ -4,13 +4,9 @@ package com.yahoo.vespa.athenz.client.zts.bindings; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.yahoo.vespa.athenz.tls.X509CertificateUtils; +import com.yahoo.vespa.athenz.client.zts.bindings.serializers.X509CertificateDeserializer; -import java.io.IOException; import java.security.cert.X509Certificate; import java.time.Instant; @@ -28,11 +24,4 @@ public class RoleCertificateResponseEntity { this.certificate = certificate; this.expiry = expiry; } - - public static class X509CertificateDeserializer extends JsonDeserializer<X509Certificate> { - @Override - public X509Certificate deserialize(JsonParser parser, DeserializationContext context) throws IOException { - return X509CertificateUtils.fromPem(parser.getValueAsString()); - } - } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/Pkcs10CsrSerializer.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/Pkcs10CsrSerializer.java new file mode 100644 index 00000000000..24825792953 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/Pkcs10CsrSerializer.java @@ -0,0 +1,20 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zts.bindings.serializers; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; +import com.yahoo.vespa.athenz.tls.Pkcs10CsrUtils; + +import java.io.IOException; + +/** + * @author bjorncs + */ +public class Pkcs10CsrSerializer extends JsonSerializer<Pkcs10Csr> { + @Override + public void serialize(Pkcs10Csr csr, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeString(Pkcs10CsrUtils.toPem(csr)); + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/X509CertificateDeserializer.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/X509CertificateDeserializer.java new file mode 100644 index 00000000000..5dd6ceb16b4 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/X509CertificateDeserializer.java @@ -0,0 +1,21 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zts.bindings.serializers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; + +import java.io.IOException; +import java.security.cert.X509Certificate; + +/** + * @author bjorncs + */ +public class X509CertificateDeserializer extends JsonDeserializer<X509Certificate> { + @Override + public X509Certificate deserialize(JsonParser parser, DeserializationContext context) throws IOException { + return X509CertificateUtils.fromPem(parser.getValueAsString()); + } +} + diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/X509CertificateListDeserializer.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/X509CertificateListDeserializer.java new file mode 100644 index 00000000000..c496031c116 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/X509CertificateListDeserializer.java @@ -0,0 +1,22 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zts.bindings.serializers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * @author bjorncs + */ +public class X509CertificateListDeserializer extends JsonDeserializer<List<X509Certificate>> { + + @Override + public List<X509Certificate> deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException { + return X509CertificateUtils.certificateListFromPem(parser.getValueAsString()); + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/package-info.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/package-info.java new file mode 100644 index 00000000000..4c442617494 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/bindings/serializers/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author bjorncs + */ +@ExportPackage +package com.yahoo.vespa.athenz.client.zts.bindings.serializers; + +import com.yahoo.osgi.annotation.ExportPackage;
\ No newline at end of file diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/utils/IdentityCsrGenerator.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/utils/IdentityCsrGenerator.java new file mode 100644 index 00000000000..2f152fafba8 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/utils/IdentityCsrGenerator.java @@ -0,0 +1,37 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zts.utils; + +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.client.zts.ZtsClient; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; +import com.yahoo.vespa.athenz.tls.Pkcs10CsrBuilder; + +import javax.security.auth.x500.X500Principal; +import java.security.KeyPair; + +import static com.yahoo.vespa.athenz.tls.SignatureAlgorithm.SHA256_WITH_RSA; + +/** + * Generates a {@link Pkcs10Csr} instance for use with {@link ZtsClient#getServiceIdentity(AthenzService, String, Pkcs10Csr)} + * + * @author bjorncs + */ +public class IdentityCsrGenerator { + + private final String dnsSuffix; + + public IdentityCsrGenerator(String dnsSuffix) { + this.dnsSuffix = dnsSuffix; + } + + public Pkcs10Csr generateIdentityCsr(AthenzService identity, KeyPair keypair) { + return Pkcs10CsrBuilder.fromKeypair(new X500Principal("CN=" + identity.getFullName()), keypair, SHA256_WITH_RSA) + .addSubjectAlternativeName(String.format( + "%s.%s.%s", + identity.getName(), + identity.getDomainName().replace(".", "-"), + dnsSuffix)) + .build(); + } + +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/utils/package-info.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/utils/package-info.java new file mode 100644 index 00000000000..baca71bc187 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/utils/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author bjorncs + */ +@ExportPackage +package com.yahoo.vespa.athenz.client.zts.utils; + +import com.yahoo.osgi.annotation.ExportPackage;
\ No newline at end of file diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java index 6ba094ff275..d96ed17765c 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java @@ -21,6 +21,7 @@ import java.io.UncheckedIOException; import java.security.GeneralSecurityException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -36,16 +37,22 @@ public class X509CertificateUtils { public static X509Certificate fromPem(String pem) { try (PEMParser parser = new PEMParser(new StringReader(pem))) { - Object pemObject = parser.readObject(); - if (pemObject instanceof X509Certificate) { - return (X509Certificate) pemObject; - } - if (pemObject instanceof X509CertificateHolder) { - return new JcaX509CertificateConverter() - .setProvider(BouncyCastleProviderHolder.getInstance()) - .getCertificate((X509CertificateHolder) pemObject); + return toX509Certificate(parser.readObject()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + public static List<X509Certificate> certificateListFromPem(String pem) { + try (PEMParser parser = new PEMParser(new StringReader(pem))) { + List<X509Certificate> list = new ArrayList<>(); + Object pemObject; + while ((pemObject = parser.readObject()) != null) { + list.add(toX509Certificate(pemObject)); } - throw new IllegalArgumentException("Invalid type of PEM object: " + pemObject); + return list; } catch (IOException e) { throw new UncheckedIOException(e); } catch (CertificateException e) { @@ -53,6 +60,18 @@ public class X509CertificateUtils { } } + private static X509Certificate toX509Certificate(Object pemObject) throws CertificateException { + if (pemObject instanceof X509Certificate) { + return (X509Certificate) pemObject; + } + if (pemObject instanceof X509CertificateHolder) { + return new JcaX509CertificateConverter() + .setProvider(BouncyCastleProviderHolder.getInstance()) + .getCertificate((X509CertificateHolder) pemObject); + } + throw new IllegalArgumentException("Invalid type of PEM object: " + pemObject); + } + public static String toPem(X509Certificate certificate) { try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { pemWriter.writeObject(new PemObject("CERTIFICATE", certificate.getEncoded())); @@ -65,6 +84,20 @@ public class X509CertificateUtils { } } + public static String toPem(List<X509Certificate> certificates) { + try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { + for (X509Certificate certificate : certificates) { + pemWriter.writeObject(new PemObject("CERTIFICATE", certificate.getEncoded())); + } + pemWriter.flush(); + return stringWriter.toString(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + public static List<String> getSubjectCommonNames(X509Certificate certificate) { return getCommonNames(certificate.getSubjectX500Principal()); } diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/TestUtils.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/TestUtils.java index 64f15408313..2a9b54f9e9e 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/TestUtils.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/TestUtils.java @@ -24,7 +24,10 @@ class TestUtils { } static X509Certificate createCertificate(KeyPair keyPair) { - X500Principal subject = new X500Principal("CN=mysubject"); + return createCertificate(keyPair, new X500Principal("CN=mysubject")); + } + + static X509Certificate createCertificate(KeyPair keyPair, X500Principal subject) { return X509CertificateBuilder .fromKeypair( keyPair, subject, Instant.now(), Instant.now().plus(1, ChronoUnit.DAYS), SignatureAlgorithm.SHA256_WITH_RSA, 1) diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java index 718c0e88972..4039bf36a5f 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java @@ -7,6 +7,7 @@ import java.security.KeyPair; import java.security.cert.X509Certificate; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Arrays; import java.util.List; import static com.yahoo.vespa.athenz.tls.SubjectAlternativeName.Type.DNS_NAME; @@ -24,15 +25,7 @@ public class X509CertificateUtilsTest { public void can_deserialize_serialized_pem_certificate() { KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); X500Principal subject = new X500Principal("CN=myservice"); - X509Certificate cert = X509CertificateBuilder - .fromKeypair( - keypair, - subject, - Instant.now(), - Instant.now().plus(1, ChronoUnit.DAYS), - SignatureAlgorithm.SHA256_WITH_RSA, - 1) - .build(); + X509Certificate cert = TestUtils.createCertificate(keypair, subject); assertEquals(subject, cert.getSubjectX500Principal()); String pem = X509CertificateUtils.toPem(cert); assertThat(pem, containsString("BEGIN CERTIFICATE")); @@ -41,6 +34,20 @@ public class X509CertificateUtilsTest { assertEquals(subject, deserializedCert.getSubjectX500Principal()); } + @Test + public void can_deserialize_serialized_pem_certificate_list() { + KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + X500Principal subject1 = new X500Principal("CN=myservice"); + X509Certificate cert1 = TestUtils.createCertificate(keypair, subject1); + X500Principal subject2 = new X500Principal("CN=myservice"); + X509Certificate cert2 = TestUtils.createCertificate(keypair, subject2); + List<X509Certificate> certificateList = Arrays.asList(cert1, cert2); + String pem = X509CertificateUtils.toPem(certificateList); + List<X509Certificate> deserializedCertificateList = X509CertificateUtils.certificateListFromPem(pem); + assertEquals(2, certificateList.size()); + assertEquals(subject1, deserializedCertificateList.get(0).getSubjectX500Principal()); + assertEquals(subject2, deserializedCertificateList.get(1).getSubjectX500Principal()); + } @Test public void can_list_subject_alternative_names() { |