From cf8af6d6ce0be3dd565b1f7a14f0648d482b3e42 Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Thu, 14 May 2020 16:58:07 +0200 Subject: Expose underlying certificate and private key from SiaIdentityProvider Extend ServiceIdentityProvider interface with new methods. Add class that bundles certificate with private key. Use Path instead of File for better compatibility with mocked file system in unit tests. --- .../com/yahoo/security/X509CertificateWithKey.java | 33 ++++++++++++++++++++++ .../security/tls/AutoReloadingX509KeyManager.java | 10 ++++++- .../athenz/identity/ServiceIdentityProvider.java | 10 +++++-- .../vespa/athenz/identity/SiaIdentityProvider.java | 32 +++++++++++++-------- .../client/AthenzIdentityProviderImpl.java | 17 +++++++++-- .../athenz/identity/SiaIdentityProviderTest.java | 12 ++++---- 6 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 security-utils/src/main/java/com/yahoo/security/X509CertificateWithKey.java diff --git a/security-utils/src/main/java/com/yahoo/security/X509CertificateWithKey.java b/security-utils/src/main/java/com/yahoo/security/X509CertificateWithKey.java new file mode 100644 index 00000000000..4772de5c1fb --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/X509CertificateWithKey.java @@ -0,0 +1,33 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; + +/** + * Wraps a {@link java.security.cert.X509Certificate} with its {@link java.security.PrivateKey}. + * Primary motivation is APIs where the callee must correctly observe an atomic update of both certificate and key. + * + * @author bjorncs + */ +public class X509CertificateWithKey { + + private final List certificate; + private final PrivateKey privateKey; + + public X509CertificateWithKey(X509Certificate certificate, PrivateKey privateKey) { + this(Collections.singletonList(certificate), privateKey); + } + + public X509CertificateWithKey(List certificate, PrivateKey privateKey) { + if (certificate.isEmpty()) throw new IllegalArgumentException(); + this.certificate = certificate; + this.privateKey = privateKey; + } + + public X509Certificate certificate() { return certificate.get(0); } + public List certificateWithIntermediates() { return certificate; } + public PrivateKey privateKey() { return privateKey; } +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/AutoReloadingX509KeyManager.java b/security-utils/src/main/java/com/yahoo/security/tls/AutoReloadingX509KeyManager.java index 18764f51dc5..d4e74e22e40 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/AutoReloadingX509KeyManager.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/AutoReloadingX509KeyManager.java @@ -5,19 +5,20 @@ import com.yahoo.security.KeyStoreBuilder; import com.yahoo.security.KeyStoreType; import com.yahoo.security.KeyUtils; import com.yahoo.security.X509CertificateUtils; +import com.yahoo.security.X509CertificateWithKey; import javax.net.ssl.SSLEngine; import javax.net.ssl.X509ExtendedKeyManager; import java.io.IOException; import java.io.UncheckedIOException; import java.net.Socket; -import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyStore; import java.security.Principal; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.time.Duration; +import java.util.Arrays; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -59,6 +60,13 @@ public class AutoReloadingX509KeyManager extends X509ExtendedKeyManager implemen return new AutoReloadingX509KeyManager(privateKeyFile, certificatesFile); } + public X509CertificateWithKey getCurrentCertificateWithKey() { + X509ExtendedKeyManager manager = mutableX509KeyManager.currentManager(); + X509Certificate[] certificateChain = manager.getCertificateChain(CERTIFICATE_ALIAS); + PrivateKey privateKey = manager.getPrivateKey(CERTIFICATE_ALIAS); + return new X509CertificateWithKey(Arrays.asList(certificateChain), privateKey); + } + private static KeyStore createKeystore(Path privateKey, Path certificateChain) { try { return KeyStoreBuilder.withType(KeyStoreType.PKCS12) diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/ServiceIdentityProvider.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/ServiceIdentityProvider.java index e5ed885b316..180d052c8dc 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/ServiceIdentityProvider.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/ServiceIdentityProvider.java @@ -2,18 +2,22 @@ package com.yahoo.vespa.athenz.identity; import com.yahoo.container.jdisc.athenz.AthenzIdentityProvider; +import com.yahoo.security.X509CertificateWithKey; import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.api.AthenzService; import javax.net.ssl.SSLContext; +import java.nio.file.Path; /** - * A interface for types that provides a service identity. - * Some similarities to {@link AthenzIdentityProvider}, but this type is not public api and intended for internal use. + * A interface for types that provides the Athenz service identity (SIA) from the environment. + * Some similarities to {@link AthenzIdentityProvider}, but this type is not public API and intended for internal use. * * @author bjorncs */ public interface ServiceIdentityProvider { AthenzIdentity identity(); SSLContext getIdentitySslContext(); + X509CertificateWithKey getIdentityCertificateWithKey(); + Path certificatePath(); + Path privateKeyPath(); } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java index 4981b80998f..082925048cb 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java @@ -5,13 +5,13 @@ import com.google.inject.Inject; import com.yahoo.component.AbstractComponent; import com.yahoo.security.KeyStoreType; import com.yahoo.security.SslContextBuilder; +import com.yahoo.security.X509CertificateWithKey; import com.yahoo.security.tls.AutoReloadingX509KeyManager; import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.utils.SiaUtils; import javax.net.ssl.SSLContext; -import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; @@ -26,34 +26,38 @@ public class SiaIdentityProvider extends AbstractComponent implements ServiceIde private final AutoReloadingX509KeyManager keyManager; private final SSLContext sslContext; private final AthenzIdentity service; + private final Path certificateFile; + private final Path privateKeyFile; @Inject public SiaIdentityProvider(SiaProviderConfig config) { this(new AthenzService(config.athenzDomain(), config.athenzService()), - SiaUtils.getPrivateKeyFile(Paths.get(config.keyPathPrefix()), new AthenzService(config.athenzDomain(), config.athenzService())).toFile(), - SiaUtils.getCertificateFile(Paths.get(config.keyPathPrefix()), new AthenzService(config.athenzDomain(), config.athenzService())).toFile(), - new File(config.trustStorePath()), + SiaUtils.getPrivateKeyFile(Paths.get(config.keyPathPrefix()), new AthenzService(config.athenzDomain(), config.athenzService())), + SiaUtils.getCertificateFile(Paths.get(config.keyPathPrefix()), new AthenzService(config.athenzDomain(), config.athenzService())), + Paths.get(config.trustStorePath()), config.trustStoreType()); } public SiaIdentityProvider(AthenzIdentity service, Path siaPath, - File trustStoreFile) { + Path trustStoreFile) { this(service, - SiaUtils.getPrivateKeyFile(siaPath, service).toFile(), - SiaUtils.getCertificateFile(siaPath, service).toFile(), + SiaUtils.getPrivateKeyFile(siaPath, service), + SiaUtils.getCertificateFile(siaPath, service), trustStoreFile, SiaProviderConfig.TrustStoreType.Enum.jks); } public SiaIdentityProvider(AthenzIdentity service, - File privateKeyFile, - File certificateFile, - File trustStoreFile, + Path privateKeyFile, + Path certificateFile, + Path trustStoreFile, SiaProviderConfig.TrustStoreType.Enum trustStoreType) { this.service = service; - this.keyManager = AutoReloadingX509KeyManager.fromPemFiles(privateKeyFile.toPath(), certificateFile.toPath()); - this.sslContext = createIdentitySslContext(keyManager, trustStoreFile.toPath(), trustStoreType); + this.keyManager = AutoReloadingX509KeyManager.fromPemFiles(privateKeyFile, certificateFile); + this.sslContext = createIdentitySslContext(keyManager, trustStoreFile, trustStoreType); + this.certificateFile = certificateFile; + this.privateKeyFile = privateKeyFile; } @Override @@ -66,6 +70,10 @@ public class SiaIdentityProvider extends AbstractComponent implements ServiceIde return sslContext; } + @Override public X509CertificateWithKey getIdentityCertificateWithKey() { return keyManager.getCurrentCertificateWithKey(); } + @Override public Path certificatePath() { return certificateFile; } + @Override public Path privateKeyPath() { return privateKeyFile; } + private static SSLContext createIdentitySslContext(AutoReloadingX509KeyManager keyManager, Path trustStoreFile, SiaProviderConfig.TrustStoreType.Enum trustStoreType) { var builder = new SslContextBuilder(); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java index 71a4c1a9954..a52ad159fdc 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java @@ -10,11 +10,11 @@ import com.yahoo.container.core.identity.IdentityConfig; import com.yahoo.container.jdisc.athenz.AthenzIdentityProvider; import com.yahoo.container.jdisc.athenz.AthenzIdentityProviderException; import com.yahoo.jdisc.Metric; -import java.util.logging.Level; import com.yahoo.security.KeyStoreBuilder; import com.yahoo.security.KeyStoreType; import com.yahoo.security.Pkcs10Csr; import com.yahoo.security.SslContextBuilder; +import com.yahoo.security.X509CertificateWithKey; import com.yahoo.security.tls.MutableX509KeyManager; import com.yahoo.vespa.athenz.api.AthenzAccessToken; import com.yahoo.vespa.athenz.api.AthenzDomain; @@ -44,6 +44,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.logging.Level; import java.util.logging.Logger; import static com.yahoo.security.KeyStoreType.JKS; @@ -55,6 +56,8 @@ import static com.yahoo.security.KeyStoreType.PKCS12; * @author mortent * @author bjorncs */ +// This class should probably not implement ServiceIdentityProvider, +// as that interface is intended for providing the node's identity, not the tenant's application identity. public final class AthenzIdentityProviderImpl extends AbstractComponent implements AthenzIdentityProvider, ServiceIdentityProvider { private static final Logger log = Logger.getLogger(AthenzIdentityProviderImpl.class.getName()); @@ -175,6 +178,16 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen return identitySslContext; } + @Override + public X509CertificateWithKey getIdentityCertificateWithKey() { + AthenzCredentials copy = this.credentials; + return new X509CertificateWithKey(copy.getCertificate(), copy.getKeyPair().getPrivate()); + } + + // The files should ideally not be used directly, must be implemented later if necessary + @Override public Path certificatePath() { throw new UnsupportedOperationException(); } + @Override public Path privateKeyPath() { throw new UnsupportedOperationException(); } + @Override public SSLContext getRoleSslContext(String domain, String role) { // This ssl context should ideally be cached as it is quite expensive to create. @@ -288,7 +301,7 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen private static SiaIdentityProvider createNodeIdentityProvider(IdentityConfig config, Path trustStore) { return new SiaIdentityProvider( - new AthenzService(config.nodeIdentityName()), SiaUtils.DEFAULT_SIA_DIRECTORY, trustStore.toFile()); + new AthenzService(config.nodeIdentityName()), SiaUtils.DEFAULT_SIA_DIRECTORY, trustStore); } private boolean isExpired(AthenzCredentials credentials) { diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identity/SiaIdentityProviderTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identity/SiaIdentityProviderTest.java index ce02860cc78..1fe32561f82 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identity/SiaIdentityProviderTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identity/SiaIdentityProviderTest.java @@ -52,9 +52,9 @@ public class SiaIdentityProviderTest { SiaIdentityProvider provider = new SiaIdentityProvider( new AthenzService("domain", "service-name"), - keyFile, - certificateFile, - trustStoreFile, + keyFile.toPath(), + certificateFile.toPath(), + trustStoreFile.toPath(), SiaProviderConfig.TrustStoreType.Enum.jks); assertNotNull(provider.getIdentitySslContext()); @@ -76,9 +76,9 @@ public class SiaIdentityProviderTest { SiaIdentityProvider provider = new SiaIdentityProvider( new AthenzService("domain", "service-name"), - keyFile, - certificateFile, - trustStoreFile, + keyFile.toPath(), + certificateFile.toPath(), + trustStoreFile.toPath(), SiaProviderConfig.TrustStoreType.Enum.pem); assertNotNull(provider.getIdentitySslContext()); -- cgit v1.2.3