diff options
author | Bjørn Christian Seime <bjorn.christian@seime.no> | 2019-02-21 09:56:57 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-02-21 09:56:57 +0100 |
commit | ee96865ec6871c35898004d0c86b56b8bad98f9b (patch) | |
tree | b94abb7c3d1be6217f3d023f891e8525666a665c | |
parent | 5d79269b8f6f74286436f09d9bdc2d3c2c38a41d (diff) | |
parent | 3de8278ac6baf34c48e895c7f6e4904e38f32808 (diff) |
Merge pull request #8568 from vespa-engine/bjorncs/jdisc-mixed-mode-preparations
Bjorncs/jdisc mixed mode preparations
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 |