summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorn.christian@seime.no>2019-02-21 09:56:57 +0100
committerGitHub <noreply@github.com>2019-02-21 09:56:57 +0100
commitee96865ec6871c35898004d0c86b56b8bad98f9b (patch)
treeb94abb7c3d1be6217f3d023f891e8525666a665c
parent5d79269b8f6f74286436f09d9bdc2d3c2c38a41d (diff)
parent3de8278ac6baf34c48e895c7f6e4904e38f32808 (diff)
Merge pull request #8568 from vespa-engine/bjorncs/jdisc-mixed-mode-preparations
Bjorncs/jdisc mixed mode preparations
-rw-r--r--jrt/src/com/yahoo/jrt/TlsCryptoEngine.java1
-rw-r--r--security-utils/pom.xml5
-rw-r--r--security-utils/src/main/java/com/yahoo/security/KeyStoreType.java2
-rw-r--r--security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java66
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/AutoReloadingX509KeyManager.java150
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/DefaultTlsContext.java90
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/KeyManagerUtils.java49
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/MutableX509KeyManager.java106
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/MutableX509TrustManager.java70
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/ReloadingTlsContext.java98
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/TlsContext.java7
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/TrustManagerUtils.java50
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java22
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManagersFactory.java8
-rw-r--r--security-utils/src/test/java/com/yahoo/security/tls/AutoReloadingX509KeyManagerTest.java84
-rw-r--r--security-utils/src/test/java/com/yahoo/security/tls/MutableX509KeyManagerTest.java65
-rw-r--r--security-utils/src/test/java/com/yahoo/security/tls/MutableX509TrustManagerTest.java59
17 files changed, 823 insertions, 109 deletions
diff --git a/jrt/src/com/yahoo/jrt/TlsCryptoEngine.java b/jrt/src/com/yahoo/jrt/TlsCryptoEngine.java
index 41302a4c725..84fbb7d4f01 100644
--- a/jrt/src/com/yahoo/jrt/TlsCryptoEngine.java
+++ b/jrt/src/com/yahoo/jrt/TlsCryptoEngine.java
@@ -22,7 +22,6 @@ public class TlsCryptoEngine implements CryptoEngine {
@Override
public TlsCryptoSocket createCryptoSocket(SocketChannel channel, boolean isServer) {
SSLEngine sslEngine = tlsContext.createSslEngine();
- sslEngine.setNeedClientAuth(true);
sslEngine.setUseClientMode(!isServer);
return new TlsCryptoSocket(channel, sslEngine);
}
diff --git a/security-utils/pom.xml b/security-utils/pom.xml
index 0a26c73cf70..10dec598915 100644
--- a/security-utils/pom.xml
+++ b/security-utils/pom.xml
@@ -54,6 +54,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git a/security-utils/src/main/java/com/yahoo/security/KeyStoreType.java b/security-utils/src/main/java/com/yahoo/security/KeyStoreType.java
index 7fb8df35286..d72bd45865d 100644
--- a/security-utils/src/main/java/com/yahoo/security/KeyStoreType.java
+++ b/security-utils/src/main/java/com/yahoo/security/KeyStoreType.java
@@ -16,7 +16,7 @@ public enum KeyStoreType {
},
PKCS12 {
KeyStore createKeystore() throws KeyStoreException {
- return KeyStore.getInstance("PKCS12", BouncyCastleProviderHolder.getInstance());
+ return KeyStore.getInstance("PKCS12");
}
};
abstract KeyStore createKeystore() throws GeneralSecurityException;
diff --git a/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java b/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java
index 09a5a87138f..1ef4df9c7bc 100644
--- a/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java
+++ b/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java
@@ -1,11 +1,14 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.security;
+import com.yahoo.security.tls.KeyManagerUtils;
+import com.yahoo.security.tls.TrustManagerUtils;
+
import javax.net.ssl.KeyManager;
-import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509ExtendedTrustManager;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
@@ -19,14 +22,17 @@ import java.util.List;
import static java.util.Collections.singletonList;
/**
+ * A builder for {@link SSLContext}.
+ *
* @author bjorncs
*/
public class SslContextBuilder {
- private KeyStoreSupplier trustStoreSupplier;
- private KeyStoreSupplier keyStoreSupplier;
+ private KeyStoreSupplier trustStoreSupplier = () -> null;
+ private KeyStoreSupplier keyStoreSupplier = () -> null;
private char[] keyStorePassword;
- private TrustManagersFactory trustManagersFactory = SslContextBuilder::createDefaultTrustManagers;
+ private TrustManagerFactory trustManagerFactory = TrustManagerUtils::createDefaultX509TrustManager;
+ private KeyManagerFactory keyManagerFactory = KeyManagerUtils::createDefaultX509KeyManager;
public SslContextBuilder() {}
@@ -94,18 +100,21 @@ public class SslContextBuilder {
return this;
}
- public SslContextBuilder withTrustManagerFactory(TrustManagersFactory trustManagersFactory) {
- this.trustManagersFactory = trustManagersFactory;
+ public SslContextBuilder withTrustManagerFactory(TrustManagerFactory trustManagersFactory) {
+ this.trustManagerFactory = trustManagersFactory;
+ return this;
+ }
+
+ public SslContextBuilder withKeyManagerFactory(KeyManagerFactory keyManagerFactory) {
+ this.keyManagerFactory = keyManagerFactory;
return this;
}
public SSLContext build() {
try {
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
- TrustManager[] trustManagers =
- trustStoreSupplier != null ? createTrustManagers(trustManagersFactory, trustStoreSupplier) : null;
- KeyManager[] keyManagers =
- keyStoreSupplier != null ? createKeyManagers(keyStoreSupplier, keyStorePassword) : null;
+ TrustManager[] trustManagers = new TrustManager[] { trustManagerFactory.createTrustManager(trustStoreSupplier.get()) };
+ KeyManager[] keyManagers = new KeyManager[] { keyManagerFactory.createKeyManager(keyStoreSupplier.get(), keyStorePassword) };
sslContext.init(keyManagers, trustManagers, null);
return sslContext;
} catch (GeneralSecurityException e) {
@@ -115,27 +124,6 @@ public class SslContextBuilder {
}
}
- private static TrustManager[] createTrustManagers(TrustManagersFactory trustManagersFactory, KeyStoreSupplier trustStoreSupplier)
- throws GeneralSecurityException, IOException {
- KeyStore truststore = trustStoreSupplier.get();
- return trustManagersFactory.createTrustManagers(truststore);
- }
-
- private static TrustManager[] createDefaultTrustManagers(KeyStore truststore) throws GeneralSecurityException {
- TrustManagerFactory trustManagerFactory =
- TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- trustManagerFactory.init(truststore);
- return trustManagerFactory.getTrustManagers();
- }
-
- private static KeyManager[] createKeyManagers(KeyStoreSupplier keyStoreSupplier, char[] password)
- throws GeneralSecurityException, IOException {
- KeyManagerFactory keyManagerFactory =
- KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- keyManagerFactory.init(keyStoreSupplier.get(), password);
- return keyManagerFactory.getKeyManagers();
- }
-
private static KeyStore createTrustStore(List<X509Certificate> caCertificates) {
KeyStoreBuilder trustStoreBuilder = KeyStoreBuilder.withType(KeyStoreType.JKS);
for (int i = 0; i < caCertificates.size(); i++) {
@@ -149,11 +137,19 @@ public class SslContextBuilder {
}
/**
- * A factory interface that is similar to {@link TrustManagerFactory}, but is an interface instead of a class.
+ * A factory interface for creating {@link X509ExtendedTrustManager}.
+ */
+ @FunctionalInterface
+ public interface TrustManagerFactory {
+ X509ExtendedTrustManager createTrustManager(KeyStore truststore) throws GeneralSecurityException;
+ }
+
+ /**
+ * A factory interface for creating {@link X509ExtendedKeyManager}.
*/
@FunctionalInterface
- public interface TrustManagersFactory {
- TrustManager[] createTrustManagers(KeyStore truststore) throws GeneralSecurityException;
+ public interface KeyManagerFactory {
+ X509ExtendedKeyManager createKeyManager(KeyStore truststore, char[] password) throws GeneralSecurityException;
}
}
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
new file mode 100644
index 00000000000..0dae185995c
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/AutoReloadingX509KeyManager.java
@@ -0,0 +1,150 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import com.yahoo.security.KeyStoreBuilder;
+import com.yahoo.security.KeyStoreType;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.X509CertificateUtils;
+
+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.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A {@link X509ExtendedKeyManager} that reloads the certificate and private key from file regularly.
+ *
+ * @author bjorncs
+ */
+public class AutoReloadingX509KeyManager extends X509ExtendedKeyManager implements AutoCloseable {
+
+ private static final Duration UPDATE_PERIOD = Duration.ofHours(1);
+
+ private static final Logger log = Logger.getLogger(AutoReloadingX509KeyManager.class.getName());
+
+ private final MutableX509KeyManager mutableX509KeyManager;
+ private final ScheduledExecutorService scheduler;
+ private final Path privateKeyFile;
+ private final Path certificatesFile;
+
+ private AutoReloadingX509KeyManager(Path privateKeyFile, Path certificatesFile) {
+ this(privateKeyFile, certificatesFile, createDefaultScheduler());
+ }
+
+ AutoReloadingX509KeyManager(Path privateKeyFile, Path certificatesFile, ScheduledExecutorService scheduler) {
+ this.privateKeyFile = privateKeyFile;
+ this.certificatesFile = certificatesFile;
+ this.scheduler = scheduler;
+ this.mutableX509KeyManager = new MutableX509KeyManager(createKeystore(privateKeyFile, certificatesFile), new char[0]);
+ scheduler.scheduleAtFixedRate(
+ new KeyManagerReloader(), UPDATE_PERIOD.getSeconds()/*initial delay*/, UPDATE_PERIOD.getSeconds(), TimeUnit.SECONDS);
+ }
+
+ public static AutoReloadingX509KeyManager fromPemFiles(Path privateKeyFile, Path certificatesFile) {
+ return new AutoReloadingX509KeyManager(privateKeyFile, certificatesFile);
+ }
+
+ private static KeyStore createKeystore(Path privateKey, Path certificateChain) {
+ try {
+ return KeyStoreBuilder.withType(KeyStoreType.PKCS12)
+ .withKeyEntry(
+ "default",
+ KeyUtils.fromPemEncodedPrivateKey(Files.readString(privateKey)),
+ X509CertificateUtils.certificateListFromPem(Files.readString(certificateChain)))
+ .build();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static ScheduledExecutorService createDefaultScheduler() {
+ return Executors.newSingleThreadScheduledExecutor(runnable -> {
+ Thread thread = new Thread(runnable, "auto-reloading-x509-key-manager");
+ thread.setDaemon(true);
+ return thread;
+ });
+ }
+
+ private class KeyManagerReloader implements Runnable {
+ @Override
+ public void run() {
+ try {
+ log.log(Level.FINE, () -> String.format("Reloading key and certificate chain (private-key='%s', certificates='%s')", privateKeyFile, certificatesFile));
+ mutableX509KeyManager.updateKeystore(createKeystore(privateKeyFile, certificatesFile), new char[0]);
+ } catch (Throwable t) {
+ log.log(Level.SEVERE,
+ String.format("Failed to load X509 key manager (private-key='%s', certificates='%s'): %s",
+ privateKeyFile, certificatesFile, t.getMessage()),
+ t);
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ scheduler.shutdownNow();
+ scheduler.awaitTermination(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ //
+ // Methods from X509ExtendedKeyManager
+ //
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers) {
+ return mutableX509KeyManager.getServerAliases(keyType, issuers);
+ }
+
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers) {
+ return mutableX509KeyManager.getClientAliases(keyType, issuers);
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+ return mutableX509KeyManager.chooseServerAlias(keyType, issuers, socket);
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+ return mutableX509KeyManager.chooseClientAlias(keyType, issuers, socket);
+ }
+
+ @Override
+ public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
+ return mutableX509KeyManager.chooseEngineServerAlias(keyType, issuers, engine);
+ }
+
+ @Override
+ public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
+ return mutableX509KeyManager.chooseEngineClientAlias(keyType, issuers, engine);
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ return mutableX509KeyManager.getCertificateChain(alias);
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ return mutableX509KeyManager.getPrivateKey(alias);
+ }
+
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/DefaultTlsContext.java b/security-utils/src/main/java/com/yahoo/security/tls/DefaultTlsContext.java
index 2befd50332a..c9c326df9ed 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/DefaultTlsContext.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/DefaultTlsContext.java
@@ -7,7 +7,7 @@ import com.yahoo.security.tls.policy.AuthorizedPeers;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
-import java.nio.file.Path;
+import javax.net.ssl.SSLParameters;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Arrays;
@@ -38,7 +38,8 @@ public class DefaultTlsContext implements TlsContext {
private static final Logger log = Logger.getLogger(DefaultTlsContext.class.getName());
private final SSLContext sslContext;
- private final List<String> acceptedCiphers;
+ private final String[] validCiphers;
+ private final String[] validProtocols;
public DefaultTlsContext(List<X509Certificate> certificates,
PrivateKey privateKey,
@@ -46,49 +47,77 @@ public class DefaultTlsContext implements TlsContext {
AuthorizedPeers authorizedPeers,
AuthorizationMode mode,
List<String> acceptedCiphers) {
- this.sslContext = createSslContext(certificates, privateKey, caCertificates, authorizedPeers, mode);
- this.acceptedCiphers = acceptedCiphers;
+ this(createSslContext(certificates, privateKey, caCertificates, authorizedPeers, mode),
+ acceptedCiphers);
}
- public DefaultTlsContext(Path tlsOptionsConfigFile, AuthorizationMode mode) {
- TransportSecurityOptions options = TransportSecurityOptions.fromJsonFile(tlsOptionsConfigFile);
- this.sslContext = createSslContext(options, mode);
- this.acceptedCiphers = options.getAcceptedCiphers();
- }
- @Override
- public SSLEngine createSslEngine() {
- SSLEngine sslEngine = sslContext.createSSLEngine();
- restrictSetOfEnabledCiphers(sslEngine, acceptedCiphers);
- restrictTlsProtocols(sslEngine);
- return sslEngine;
+ public DefaultTlsContext(SSLContext sslContext, List<String> acceptedCiphers) {
+ this.sslContext = sslContext;
+ this.validCiphers = getAllowedCiphers(sslContext, acceptedCiphers);
+ this.validProtocols = getAllowedProtocols(sslContext);
}
- private static void restrictSetOfEnabledCiphers(SSLEngine sslEngine, List<String> acceptedCiphers) {
- String[] validCipherSuites = Arrays.stream(sslEngine.getSupportedCipherSuites())
+
+ private static String[] getAllowedCiphers(SSLContext sslContext, List<String> acceptedCiphers) {
+ String[] supportedCipherSuites = sslContext.getSupportedSSLParameters().getCipherSuites();
+ String[] validCipherSuites = Arrays.stream(supportedCipherSuites)
.filter(suite -> ALLOWED_CIPHER_SUITES.contains(suite) && (acceptedCiphers.isEmpty() || acceptedCiphers.contains(suite)))
.toArray(String[]::new);
if (validCipherSuites.length == 0) {
throw new IllegalStateException(
String.format("None of the allowed cipher suites are supported " +
"(allowed-cipher-suites=%s, supported-cipher-suites=%s, accepted-cipher-suites=%s)",
- ALLOWED_CIPHER_SUITES, List.of(sslEngine.getSupportedCipherSuites()), acceptedCiphers));
+ ALLOWED_CIPHER_SUITES, List.of(supportedCipherSuites), acceptedCiphers));
}
- log.log(Level.FINE, () -> String.format("Allowed cipher suites that are supported: %s", Arrays.toString(validCipherSuites)));
- sslEngine.setEnabledCipherSuites(validCipherSuites);
+ log.log(Level.FINE, () -> String.format("Allowed cipher suites that are supported: %s", List.of(validCipherSuites)));
+ return validCipherSuites;
}
- private static void restrictTlsProtocols(SSLEngine sslEngine) {
- String[] validProtocols = Arrays.stream(sslEngine.getSupportedProtocols())
+ private static String[] getAllowedProtocols(SSLContext sslContext) {
+ String[] supportedProtocols = sslContext.getSupportedSSLParameters().getProtocols();
+ String[] validProtocols = Arrays.stream(supportedProtocols)
.filter(ALLOWED_PROTOCOLS::contains)
.toArray(String[]::new);
if (validProtocols.length == 0) {
throw new IllegalArgumentException(
String.format("None of the allowed protocols are supported (allowed-protocols=%s, supported-protocols=%s)",
- ALLOWED_PROTOCOLS, Arrays.toString(sslEngine.getSupportedProtocols())));
+ ALLOWED_PROTOCOLS, List.of(supportedProtocols)));
}
- log.log(Level.FINE, () -> String.format("Allowed protocols that are supported: %s", Arrays.toString(validProtocols)));
- sslEngine.setEnabledProtocols(validProtocols);
+ log.log(Level.FINE, () -> String.format("Allowed protocols that are supported: %s", List.of(validProtocols)));
+ return validProtocols;
+ }
+
+ @Override
+ public SSLContext context() {
+ return sslContext;
+ }
+
+ @Override
+ public SSLParameters parameters() {
+ return createSslParameters();
+ }
+
+ @Override
+ public SSLEngine createSslEngine() {
+ SSLEngine sslEngine = sslContext.createSSLEngine();
+ sslEngine.setSSLParameters(createSslParameters());
+ return sslEngine;
+ }
+
+ @Override
+ public SSLEngine createSslEngine(String peerHost, int peerPort) {
+ SSLEngine sslEngine = sslContext.createSSLEngine(peerHost, peerPort);
+ sslEngine.setSSLParameters(createSslParameters());
+ return sslEngine;
+ }
+
+ private SSLParameters createSslParameters() {
+ SSLParameters newParameters = sslContext.getDefaultSSLParameters();
+ newParameters.setCipherSuites(validCiphers);
+ newParameters.setProtocols(validProtocols);
+ newParameters.setNeedClientAuth(true);
+ return newParameters;
}
private static SSLContext createSslContext(List<X509Certificate> certificates,
@@ -109,16 +138,5 @@ public class DefaultTlsContext implements TlsContext {
return builder.build();
}
- private static SSLContext createSslContext(TransportSecurityOptions options, AuthorizationMode mode) {
- SslContextBuilder builder = new SslContextBuilder();
- options.getCertificatesFile()
- .ifPresent(certificates -> builder.withKeyStore(options.getPrivateKeyFile().get(), certificates));
- options.getCaCertificatesFile().ifPresent(builder::withTrustStore);
- if (mode != AuthorizationMode.DISABLE) {
- options.getAuthorizedPeers().ifPresent(
- authorizedPeers -> builder.withTrustManagerFactory(new PeerAuthorizerTrustManagersFactory(authorizedPeers, mode)));
- }
- return builder.build();
- }
}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/KeyManagerUtils.java b/security-utils/src/main/java/com/yahoo/security/tls/KeyManagerUtils.java
new file mode 100644
index 00000000000..2e48de3c01f
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/KeyManagerUtils.java
@@ -0,0 +1,49 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import com.yahoo.security.KeyStoreBuilder;
+import com.yahoo.security.KeyStoreType;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.X509ExtendedKeyManager;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility methods for constructing {@link X509ExtendedKeyManager}.
+ *
+ * @author bjorncs
+ */
+public class KeyManagerUtils {
+
+ public static X509ExtendedKeyManager createDefaultX509KeyManager(KeyStore keystore, char[] password) {
+ try {
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keystore, password);
+ KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
+ return Arrays.stream(keyManagers)
+ .filter(manager -> manager instanceof X509ExtendedKeyManager)
+ .map(X509ExtendedKeyManager.class::cast)
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("No X509ExtendedKeyManager in " + List.of(keyManagers)));
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static X509ExtendedKeyManager createDefaultX509KeyManager(PrivateKey privateKey, List<X509Certificate> certificateChain) {
+ KeyStore keystore = KeyStoreBuilder.withType(KeyStoreType.PKCS12)
+ .withKeyEntry("default", privateKey, certificateChain)
+ .build();
+ return createDefaultX509KeyManager(keystore, new char[0]);
+ }
+
+ public static X509ExtendedKeyManager createDefaultX509KeyManager() {
+ return createDefaultX509KeyManager(null, new char[0]);
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/MutableX509KeyManager.java b/security-utils/src/main/java/com/yahoo/security/tls/MutableX509KeyManager.java
new file mode 100644
index 00000000000..e5e56f7a181
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/MutableX509KeyManager.java
@@ -0,0 +1,106 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
+import java.net.Socket;
+import java.security.KeyStore;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.WeakHashMap;
+
+/**
+ * A {@link X509ExtendedKeyManager} which can be updated with new certificate chain and private key while in use.
+ *
+ * The implementations assumes that aliases are retrieved from the same thread as the certificate chain and private key.
+ * This is case for OpenJDK 11.
+ *
+ * @author bjorncs
+ */
+public class MutableX509KeyManager extends X509ExtendedKeyManager {
+
+ // Not using ThreadLocal as we want the x509 key manager instances to be collected
+ // when either the thread dies or the MutableX509KeyManager instance is collected (latter not the case for ThreadLocal).
+ private final WeakHashMap<Thread, X509ExtendedKeyManager> threadLocalManager = new WeakHashMap<>();
+ private volatile X509ExtendedKeyManager currentManager;
+
+ public MutableX509KeyManager(KeyStore keystore, char[] password) {
+ this.currentManager = KeyManagerUtils.createDefaultX509KeyManager(keystore, password);
+ }
+
+ public MutableX509KeyManager() {
+ this.currentManager = KeyManagerUtils.createDefaultX509KeyManager();
+ }
+
+ public void updateKeystore(KeyStore keystore, char[] password) {
+ this.currentManager = KeyManagerUtils.createDefaultX509KeyManager(keystore, password);
+ }
+
+ public void useDefaultKeystore() {
+ this.currentManager = KeyManagerUtils.createDefaultX509KeyManager();
+ }
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers) {
+ return updateAndGetThreadLocalManager()
+ .getServerAliases(keyType, issuers);
+ }
+
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers) {
+ return updateAndGetThreadLocalManager()
+ .getClientAliases(keyType, issuers);
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+ return updateAndGetThreadLocalManager()
+ .chooseServerAlias(keyType, issuers, socket);
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+ return updateAndGetThreadLocalManager()
+ .chooseClientAlias(keyType, issuers, socket);
+ }
+
+ @Override
+ public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
+ return updateAndGetThreadLocalManager()
+ .chooseEngineServerAlias(keyType, issuers, engine);
+ }
+
+ @Override
+ public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
+ return updateAndGetThreadLocalManager()
+ .chooseEngineClientAlias(keyType, issuers, engine);
+ }
+
+ private X509ExtendedKeyManager updateAndGetThreadLocalManager() {
+ X509ExtendedKeyManager currentManager = this.currentManager;
+ threadLocalManager.put(Thread.currentThread(), currentManager);
+ return currentManager;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ return getThreadLocalManager()
+ .getCertificateChain(alias);
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ return getThreadLocalManager()
+ .getPrivateKey(alias);
+ }
+
+ private X509ExtendedKeyManager getThreadLocalManager() {
+ X509ExtendedKeyManager manager = threadLocalManager.get(Thread.currentThread());
+ if (manager == null) {
+ throw new IllegalStateException("Methods to retrieve valid aliases has not been called previously from this thread");
+ }
+ return manager;
+ }
+
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/MutableX509TrustManager.java b/security-utils/src/main/java/com/yahoo/security/tls/MutableX509TrustManager.java
new file mode 100644
index 00000000000..ed424480d26
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/MutableX509TrustManager.java
@@ -0,0 +1,70 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedTrustManager;
+import java.net.Socket;
+import java.security.KeyStore;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * A {@link X509ExtendedTrustManager} which can be updated with new CA certificates while in use.
+ *
+ * @author bjorncs
+ */
+public class MutableX509TrustManager extends X509ExtendedTrustManager {
+
+ private volatile X509ExtendedTrustManager currentManager;
+
+ public MutableX509TrustManager(KeyStore truststore) {
+ this.currentManager = TrustManagerUtils.createDefaultX509TrustManager(truststore);
+ }
+
+ public MutableX509TrustManager() {
+ this.currentManager = TrustManagerUtils.createDefaultX509TrustManager();
+ }
+
+ public void updateTruststore(KeyStore truststore) {
+ this.currentManager = TrustManagerUtils.createDefaultX509TrustManager(truststore);
+ }
+
+ public void useDefaultTruststore() {
+ this.currentManager = TrustManagerUtils.createDefaultX509TrustManager();
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ currentManager.checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ currentManager.checkServerTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
+ currentManager.checkClientTrusted(chain, authType, socket);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
+ currentManager.checkServerTrusted(chain, authType, socket);
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
+ currentManager.checkClientTrusted(chain, authType, sslEngine);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
+ currentManager.checkServerTrusted(chain, authType, sslEngine);
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return currentManager.getAcceptedIssuers();
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/ReloadingTlsContext.java b/security-utils/src/main/java/com/yahoo/security/tls/ReloadingTlsContext.java
index 5add13e067d..b57105f54f9 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/ReloadingTlsContext.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/ReloadingTlsContext.java
@@ -1,13 +1,28 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.security.tls;
+import com.yahoo.security.KeyStoreBuilder;
+import com.yahoo.security.KeyStoreType;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SslContextBuilder;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.security.tls.authz.PeerAuthorizerTrustManager;
+
+import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.X509ExtendedTrustManager;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
import java.nio.file.Path;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
import java.time.Duration;
+import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -23,8 +38,9 @@ public class ReloadingTlsContext implements TlsContext {
private static final Logger log = Logger.getLogger(ReloadingTlsContext.class.getName());
private final Path tlsOptionsConfigFile;
- private final AuthorizationMode mode;
- private final AtomicReference<TlsContext> currentTlsContext;
+ private final TlsContext tlsContext;
+ private final MutableX509TrustManager trustManager = new MutableX509TrustManager();
+ private final MutableX509KeyManager keyManager = new MutableX509KeyManager();
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable, "tls-context-reloader");
@@ -34,19 +50,77 @@ public class ReloadingTlsContext implements TlsContext {
public ReloadingTlsContext(Path tlsOptionsConfigFile, AuthorizationMode mode) {
this.tlsOptionsConfigFile = tlsOptionsConfigFile;
- this.mode = mode;
- this.currentTlsContext = new AtomicReference<>(new DefaultTlsContext(tlsOptionsConfigFile, mode));
- this.scheduler.scheduleAtFixedRate(new SslContextReloader(),
+ TransportSecurityOptions options = TransportSecurityOptions.fromJsonFile(tlsOptionsConfigFile);
+ reloadCryptoMaterial(options, trustManager, keyManager);
+ this.tlsContext = createDefaultTlsContext(options, mode, trustManager, keyManager);
+ this.scheduler.scheduleAtFixedRate(new CryptoMaterialReloader(),
UPDATE_PERIOD.getSeconds()/*initial delay*/,
UPDATE_PERIOD.getSeconds(),
TimeUnit.SECONDS);
}
- @Override
- public SSLEngine createSslEngine() {
- return currentTlsContext.get().createSslEngine();
+ private static void reloadCryptoMaterial(TransportSecurityOptions options,
+ MutableX509TrustManager trustManager,
+ MutableX509KeyManager keyManager) {
+ if (options.getCaCertificatesFile().isPresent()) {
+ trustManager.updateTruststore(loadTruststore(options.getCaCertificatesFile().get()));
+ } else {
+ trustManager.useDefaultTruststore();
+ }
+
+ if (options.getPrivateKeyFile().isPresent() && options.getCertificatesFile().isPresent()) {
+ keyManager.updateKeystore(loadKeystore(options.getPrivateKeyFile().get(), options.getCertificatesFile().get()), new char[0]);
+ } else {
+ keyManager.useDefaultKeystore();
+ }
}
+ private static KeyStore loadTruststore(Path caCertificateFile) {
+ try {
+ List<X509Certificate> caCertificates = X509CertificateUtils.certificateListFromPem(Files.readString(caCertificateFile));
+ KeyStoreBuilder trustStoreBuilder = KeyStoreBuilder.withType(KeyStoreType.PKCS12);
+ for (int i = 0; i < caCertificates.size(); i++) {
+ trustStoreBuilder.withCertificateEntry("cert-" + i, caCertificates.get(i));
+ }
+ return trustStoreBuilder.build();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static KeyStore loadKeystore(Path privateKeyFile, Path certificatesFile) {
+ try {
+ return KeyStoreBuilder.withType(KeyStoreType.PKCS12)
+ .withKeyEntry(
+ "default",
+ KeyUtils.fromPemEncodedPrivateKey(Files.readString(privateKeyFile)),
+ X509CertificateUtils.certificateListFromPem(Files.readString(certificatesFile)))
+ .build();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static DefaultTlsContext createDefaultTlsContext(TransportSecurityOptions options,
+ AuthorizationMode mode,
+ MutableX509TrustManager mutableTrustManager,
+ MutableX509KeyManager mutableKeyManager) {
+ SSLContext sslContext = new SslContextBuilder()
+ .withKeyManagerFactory((ignoredKeystore, ignoredPassword) -> mutableKeyManager)
+ .withTrustManagerFactory(
+ ignoredTruststore -> options.getAuthorizedPeers()
+ .map(authorizedPeers -> (X509ExtendedTrustManager) new PeerAuthorizerTrustManager(authorizedPeers, mode, mutableTrustManager))
+ .orElse(mutableTrustManager))
+ .build();
+ return new DefaultTlsContext(sslContext, options.getAcceptedCiphers());
+ }
+
+ // Wrapped methods from TlsContext
+ @Override public SSLContext context() { return tlsContext.context(); }
+ @Override public SSLParameters parameters() { return tlsContext.parameters(); }
+ @Override public SSLEngine createSslEngine() { return tlsContext.createSslEngine(); }
+ @Override public SSLEngine createSslEngine(String peerHost, int peerPort) { return tlsContext.createSslEngine(peerHost, peerPort); }
+
@Override
public void close() {
try {
@@ -57,13 +131,13 @@ public class ReloadingTlsContext implements TlsContext {
}
}
- private class SslContextReloader implements Runnable {
+ private class CryptoMaterialReloader implements Runnable {
@Override
public void run() {
try {
- currentTlsContext.set(new DefaultTlsContext(tlsOptionsConfigFile, mode));
+ reloadCryptoMaterial(TransportSecurityOptions.fromJsonFile(tlsOptionsConfigFile), trustManager, keyManager);
} catch (Throwable t) {
- log.log(Level.SEVERE, String.format("Failed to load SSLContext (path='%s'): %s", tlsOptionsConfigFile, t.getMessage()), t);
+ log.log(Level.SEVERE, String.format("Failed to reload crypto material (path='%s'): %s", tlsOptionsConfigFile, t.getMessage()), t);
}
}
}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TlsContext.java b/security-utils/src/main/java/com/yahoo/security/tls/TlsContext.java
index 58687a0ba8f..b315dd00b31 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/TlsContext.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/TlsContext.java
@@ -3,6 +3,7 @@ package com.yahoo.security.tls;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
/**
* A simplified version of {@link SSLContext} modelled as an interface.
@@ -11,8 +12,14 @@ import javax.net.ssl.SSLEngine;
*/
public interface TlsContext extends AutoCloseable {
+ SSLContext context();
+
+ SSLParameters parameters();
+
SSLEngine createSslEngine();
+ SSLEngine createSslEngine(String peerHost, int peerPort);
+
@Override default void close() {}
}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TrustManagerUtils.java b/security-utils/src/main/java/com/yahoo/security/tls/TrustManagerUtils.java
new file mode 100644
index 00000000000..f114b672ed8
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/TrustManagerUtils.java
@@ -0,0 +1,50 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import com.yahoo.security.KeyStoreBuilder;
+import com.yahoo.security.KeyStoreType;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedTrustManager;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility methods for constructing {@link X509ExtendedTrustManager}.
+ *
+ * @author bjorncs
+ */
+public class TrustManagerUtils {
+
+ public static X509ExtendedTrustManager createDefaultX509TrustManager(KeyStore truststore) {
+ try {
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(truststore);
+ TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+ return Arrays.stream(trustManagers)
+ .filter(manager -> manager instanceof X509ExtendedTrustManager)
+ .map(X509ExtendedTrustManager.class::cast)
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("No X509ExtendedTrustManager in " + List.of(trustManagers)));
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static X509ExtendedTrustManager createDefaultX509TrustManager(List<X509Certificate> certificates) {
+ KeyStoreBuilder truststoreBuilder = KeyStoreBuilder.withType(KeyStoreType.PKCS12);
+ for (int i = 0; i < certificates.size(); i++) {
+ truststoreBuilder.withCertificateEntry("cert-" + i, certificates.get(i));
+ }
+ KeyStore truststore = truststoreBuilder.build();
+ return createDefaultX509TrustManager(truststore);
+ }
+
+ public static X509ExtendedTrustManager createDefaultX509TrustManager() {
+ return createDefaultX509TrustManager((KeyStore) null);
+ }
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java
index 80acc940a99..eee2e502183 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java
@@ -3,14 +3,12 @@ package com.yahoo.security.tls.authz;
import com.yahoo.security.X509CertificateUtils;
import com.yahoo.security.tls.AuthorizationMode;
+import com.yahoo.security.tls.TrustManagerUtils;
import com.yahoo.security.tls.policy.AuthorizedPeers;
import javax.net.ssl.SSLEngine;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedTrustManager;
import java.net.Socket;
-import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
@@ -39,22 +37,8 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager {
this.defaultTrustManager = defaultTrustManager;
}
- public static TrustManager[] wrapTrustManagersFromKeystore(AuthorizedPeers authorizedPeers, AuthorizationMode mode, KeyStore keystore) throws GeneralSecurityException {
- TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- factory.init(keystore);
- return wrapTrustManagers(authorizedPeers, mode, factory.getTrustManagers());
- }
-
- public static TrustManager[] wrapTrustManagers(AuthorizedPeers authorizedPeers, AuthorizationMode mode, TrustManager[] managers) {
- TrustManager[] wrappedManagers = new TrustManager[managers.length];
- for (int i = 0; i < managers.length; i++) {
- if (managers[i] instanceof X509ExtendedTrustManager) {
- wrappedManagers[i] = new PeerAuthorizerTrustManager(authorizedPeers, mode, (X509ExtendedTrustManager) managers[i]);
- } else {
- wrappedManagers[i] = managers[i];
- }
- }
- return wrappedManagers;
+ public PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, AuthorizationMode mode, KeyStore truststore) {
+ this(authorizedPeers, mode, TrustManagerUtils.createDefaultX509TrustManager(truststore));
}
@Override
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManagersFactory.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManagersFactory.java
index c0a3b4e41a5..6ec8450c035 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManagersFactory.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManagersFactory.java
@@ -5,14 +5,12 @@ import com.yahoo.security.SslContextBuilder;
import com.yahoo.security.tls.AuthorizationMode;
import com.yahoo.security.tls.policy.AuthorizedPeers;
-import javax.net.ssl.TrustManager;
-import java.security.GeneralSecurityException;
import java.security.KeyStore;
/**
* @author bjorncs
*/
-public class PeerAuthorizerTrustManagersFactory implements SslContextBuilder.TrustManagersFactory {
+public class PeerAuthorizerTrustManagersFactory implements SslContextBuilder.TrustManagerFactory {
private final AuthorizedPeers authorizedPeers;
private AuthorizationMode mode;
@@ -22,7 +20,7 @@ public class PeerAuthorizerTrustManagersFactory implements SslContextBuilder.Tru
}
@Override
- public TrustManager[] createTrustManagers(KeyStore truststore) throws GeneralSecurityException {
- return PeerAuthorizerTrustManager.wrapTrustManagersFromKeystore(authorizedPeers, mode, truststore);
+ public PeerAuthorizerTrustManager createTrustManager(KeyStore truststore) {
+ return new PeerAuthorizerTrustManager(authorizedPeers, mode, truststore);
}
}
diff --git a/security-utils/src/test/java/com/yahoo/security/tls/AutoReloadingX509KeyManagerTest.java b/security-utils/src/test/java/com/yahoo/security/tls/AutoReloadingX509KeyManagerTest.java
new file mode 100644
index 00000000000..139d5313074
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/tls/AutoReloadingX509KeyManagerTest.java
@@ -0,0 +1,84 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import com.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SignatureAlgorithm;
+import com.yahoo.security.X509CertificateBuilder;
+import com.yahoo.security.X509CertificateUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.Principal;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.concurrent.ScheduledExecutorService;
+
+import static java.time.temporal.ChronoUnit.DAYS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Mockito.verify;
+
+/**
+ * @author bjorncs
+ */
+public class AutoReloadingX509KeyManagerTest {
+ private static final X500Principal SUBJECT = new X500Principal("CN=dummy");
+
+ @Rule
+ public TemporaryFolder tempDirectory = new TemporaryFolder();
+
+ @Test
+ public void crypto_material_is_reloaded_when_scheduler_task_is_executed() throws IOException {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
+ Path privateKeyFile = tempDirectory.newFile().toPath();
+ Files.writeString(privateKeyFile, KeyUtils.toPem(keyPair.getPrivate()));
+
+ Path certificateFile = tempDirectory.newFile().toPath();
+ BigInteger serialNumberInitialCertificate = BigInteger.ONE;
+ X509Certificate initialCertificate = generateCertificate(keyPair, serialNumberInitialCertificate);
+ Files.writeString(certificateFile, X509CertificateUtils.toPem(initialCertificate));
+
+ ScheduledExecutorService scheduler = Mockito.mock(ScheduledExecutorService.class);
+ ArgumentCaptor<Runnable> updaterTaskCaptor = ArgumentCaptor.forClass(Runnable.class);
+
+ AutoReloadingX509KeyManager keyManager = new AutoReloadingX509KeyManager(privateKeyFile, certificateFile, scheduler);
+ verify(scheduler).scheduleAtFixedRate(updaterTaskCaptor.capture(), anyLong(), anyLong(), any());
+
+ String[] initialAliases = keyManager.getClientAliases(keyPair.getPublic().getAlgorithm(), new Principal[]{SUBJECT});
+ X509Certificate[] certChain = keyManager.getCertificateChain(initialAliases[0]);
+ assertThat(certChain).hasSize(1);
+ assertThat(certChain[0].getSerialNumber()).isEqualTo(serialNumberInitialCertificate);
+
+ BigInteger serialNumberUpdatedCertificate = BigInteger.TWO;
+ X509Certificate updatedCertificate = generateCertificate(keyPair, serialNumberUpdatedCertificate);
+ Files.writeString(certificateFile, X509CertificateUtils.toPem(updatedCertificate));
+
+ updaterTaskCaptor.getValue().run(); // run update task in ReloadingX509KeyManager
+
+ String[] updatedAliases = keyManager.getClientAliases(keyPair.getPublic().getAlgorithm(), new Principal[]{SUBJECT});
+ X509Certificate[] updatedCertChain = keyManager.getCertificateChain(updatedAliases[0]);
+ assertThat(updatedCertChain).hasSize(1);
+ assertThat(updatedCertChain[0].getSerialNumber()).isEqualTo(serialNumberUpdatedCertificate);
+ }
+
+ private static X509Certificate generateCertificate(KeyPair keyPair, BigInteger serialNumber) {
+ return X509CertificateBuilder.fromKeypair(keyPair,
+ SUBJECT,
+ Instant.EPOCH,
+ Instant.EPOCH.plus(1, DAYS),
+ SignatureAlgorithm.SHA256_WITH_ECDSA,
+ serialNumber)
+ .build();
+ }
+} \ No newline at end of file
diff --git a/security-utils/src/test/java/com/yahoo/security/tls/MutableX509KeyManagerTest.java b/security-utils/src/test/java/com/yahoo/security/tls/MutableX509KeyManagerTest.java
new file mode 100644
index 00000000000..30e54d3c09d
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/tls/MutableX509KeyManagerTest.java
@@ -0,0 +1,65 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import com.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyStoreBuilder;
+import com.yahoo.security.KeyStoreType;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SignatureAlgorithm;
+import com.yahoo.security.X509CertificateBuilder;
+import org.junit.Test;
+
+import javax.security.auth.x500.X500Principal;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.Principal;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+
+import static java.time.temporal.ChronoUnit.DAYS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author bjorncs
+ */
+public class MutableX509KeyManagerTest {
+
+ private static final X500Principal SUBJECT = new X500Principal("CN=dummy");
+
+ @Test
+ public void key_manager_can_be_updated_with_new_certificate() {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
+
+ BigInteger serialNumberInitialCertificate = BigInteger.ONE;
+ KeyStore initialKeystore = generateKeystore(keyPair, serialNumberInitialCertificate);
+
+ MutableX509KeyManager keyManager = new MutableX509KeyManager(initialKeystore, new char[0]);
+
+ String[] initialAliases = keyManager.getClientAliases(keyPair.getPublic().getAlgorithm(), new Principal[]{SUBJECT});
+ assertThat(initialAliases).hasSize(1);
+ X509Certificate[] certChain = keyManager.getCertificateChain(initialAliases[0]);
+ assertThat(certChain).hasSize(1);
+ assertThat(certChain[0].getSerialNumber()).isEqualTo(serialNumberInitialCertificate);
+
+ BigInteger serialNumberUpdatedCertificate = BigInteger.TWO;
+ KeyStore updatedKeystore = generateKeystore(keyPair, serialNumberUpdatedCertificate);
+ keyManager.updateKeystore(updatedKeystore, new char[0]);
+
+ String[] updatedAliases = keyManager.getClientAliases(keyPair.getPublic().getAlgorithm(), new Principal[]{SUBJECT});
+ assertThat(updatedAliases).hasSize(1);
+ X509Certificate[] updatedCertChain = keyManager.getCertificateChain(updatedAliases[0]);
+ assertThat(updatedCertChain).hasSize(1);
+ assertThat(updatedCertChain[0].getSerialNumber()).isEqualTo(serialNumberUpdatedCertificate);
+ }
+
+ private static KeyStore generateKeystore(KeyPair keyPair, BigInteger serialNumber) {
+ X509Certificate certificate = X509CertificateBuilder.fromKeypair(
+ keyPair, SUBJECT, Instant.EPOCH, Instant.EPOCH.plus(1, DAYS), SignatureAlgorithm.SHA256_WITH_ECDSA, serialNumber)
+ .build();
+ return KeyStoreBuilder.withType(KeyStoreType.PKCS12)
+ .withKeyEntry("default", keyPair.getPrivate(), certificate)
+ .build();
+ }
+
+} \ No newline at end of file
diff --git a/security-utils/src/test/java/com/yahoo/security/tls/MutableX509TrustManagerTest.java b/security-utils/src/test/java/com/yahoo/security/tls/MutableX509TrustManagerTest.java
new file mode 100644
index 00000000000..4c4ea332818
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/tls/MutableX509TrustManagerTest.java
@@ -0,0 +1,59 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import com.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyStoreBuilder;
+import com.yahoo.security.KeyStoreType;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SignatureAlgorithm;
+import com.yahoo.security.X509CertificateBuilder;
+import org.junit.Test;
+
+import javax.security.auth.x500.X500Principal;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+
+import static java.time.temporal.ChronoUnit.DAYS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author bjorncs
+ */
+public class MutableX509TrustManagerTest {
+
+ @Test
+ public void key_manager_can_be_updated_with_new_certificate() {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
+
+ X509Certificate initialCertificate = generateCertificate(new X500Principal("CN=issuer1"), keyPair);
+ KeyStore initialTruststore = generateTruststore(initialCertificate);
+
+ MutableX509TrustManager trustManager = new MutableX509TrustManager(initialTruststore);
+
+ X509Certificate[] initialAcceptedIssuers = trustManager.getAcceptedIssuers();
+ assertThat(initialAcceptedIssuers).containsExactly(initialCertificate);
+
+ X509Certificate updatedCertificate = generateCertificate(new X500Principal("CN=issuer2"), keyPair);
+ KeyStore updatedTruststore = generateTruststore(updatedCertificate);
+ trustManager.updateTruststore(updatedTruststore);
+
+ X509Certificate[] updatedAcceptedIssuers = trustManager.getAcceptedIssuers();
+ assertThat(updatedAcceptedIssuers).containsExactly(updatedCertificate);
+ }
+
+ private static X509Certificate generateCertificate(X500Principal issuer, KeyPair keyPair) {
+ return X509CertificateBuilder.fromKeypair(
+ keyPair, issuer, Instant.EPOCH, Instant.EPOCH.plus(1, DAYS), SignatureAlgorithm.SHA256_WITH_ECDSA, BigInteger.ONE)
+ .build();
+ }
+
+ private static KeyStore generateTruststore(X509Certificate certificate) {
+ return KeyStoreBuilder.withType(KeyStoreType.PKCS12)
+ .withCertificateEntry("default", certificate)
+ .build();
+ }
+
+} \ No newline at end of file