diff --git a/src/main/java/io/vertx/core/net/KeyStoreOptionsBase.java b/src/main/java/io/vertx/core/net/KeyStoreOptionsBase.java index 16907ad8b07..cac345b20de 100644 --- a/src/main/java/io/vertx/core/net/KeyStoreOptionsBase.java +++ b/src/main/java/io/vertx/core/net/KeyStoreOptionsBase.java @@ -15,8 +15,10 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.impl.VertxInternal; import io.vertx.core.net.impl.KeyStoreHelper; +import io.vertx.core.net.impl.ReloadingKeyStore; import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.KeyStoreBuilderParameters; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509KeyManager; @@ -40,6 +42,8 @@ public abstract class KeyStoreOptionsBase implements KeyCertOptions, TrustOption private Buffer value; private String alias; private String aliasPassword; + private X509KeyManager km; + private KeyStore.Builder builder; /** * Default constructor @@ -189,6 +193,29 @@ KeyStoreHelper getHelper(Vertx vertx) throws Exception { return helper; } + X509KeyManager getKeyManager(Vertx vertx) throws Exception { + if (km == null) { + VertxInternal v = (VertxInternal) vertx; + + if (this.path != null) { + builder = ReloadingKeyStore.Builder.fromKeyStoreFile(v, type, provider, + v.resolveFile(path).getAbsolutePath(), password, alias, aliasPassword); + } else if (this.value != null) { + + KeyStore.ProtectionParameter protection = new KeyStore.PasswordProtection( + password != null ? password.toCharArray() : null); + + builder = KeyStore.Builder + .newInstance(KeyStoreHelper.loadKeyStore(type, provider, password, this::getValue, alias), protection); + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("NewSunX509"); + kmf.init(new KeyStoreBuilderParameters(builder)); + km = (X509KeyManager) kmf.getKeyManagers()[0]; + } + return km; + } + /** * Load and return a Java keystore. * @@ -196,20 +223,21 @@ KeyStoreHelper getHelper(Vertx vertx) throws Exception { * @return the {@code KeyStore} */ public KeyStore loadKeyStore(Vertx vertx) throws Exception { - KeyStoreHelper helper = getHelper(vertx); - return helper != null ? helper.store() : null; + // Ensure that KeyStore is constructed. + getKeyManager(vertx); + return builder.getKeyStore(); } @Override public KeyManagerFactory getKeyManagerFactory(Vertx vertx) throws Exception { - KeyStoreHelper helper = getHelper(vertx); - return helper != null ? helper.getKeyMgrFactory() : null; + return new KeyManagerFactoryWrapper(getKeyManager(vertx)); } @Override public Function keyManagerMapper(Vertx vertx) throws Exception { - KeyStoreHelper helper = getHelper(vertx); - return helper != null ? helper::getKeyMgr : null; + X509KeyManager km = getKeyManager(vertx); + // Key manager will do SNI lookup and mapping from SNI server name to certificate and key alias. + return serverName -> km; } @Override diff --git a/src/main/java/io/vertx/core/net/PemKeyCertOptions.java b/src/main/java/io/vertx/core/net/PemKeyCertOptions.java index 681010355a0..3665c7c717b 100644 --- a/src/main/java/io/vertx/core/net/PemKeyCertOptions.java +++ b/src/main/java/io/vertx/core/net/PemKeyCertOptions.java @@ -19,13 +19,16 @@ import io.vertx.core.impl.VertxInternal; import io.vertx.core.json.JsonObject; import io.vertx.core.net.impl.KeyStoreHelper; +import io.vertx.core.net.impl.ReloadingKeyStore; import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.KeyStoreBuilderParameters; import javax.net.ssl.X509KeyManager; import java.security.KeyStore; import java.util.ArrayList; import java.util.List; import java.util.function.Function; +import java.util.stream.Collectors; /** * Key store options configuring a list of private key and its certificate based on @@ -97,7 +100,9 @@ @DataObject(generateConverter = true, publicConverter = false) public class PemKeyCertOptions implements KeyCertOptions { - private KeyStoreHelper helper; + private X509KeyManager km; + private KeyStore.Builder builder; + private List keyPaths; private List keyValues; private List certPaths; @@ -390,21 +395,20 @@ public PemKeyCertOptions copy() { return new PemKeyCertOptions(this); } - KeyStoreHelper getHelper(Vertx vertx) throws Exception { - if (helper == null) { - List keys = new ArrayList<>(); - for (String keyPath : keyPaths) { - keys.add(vertx.fileSystem().readFileBlocking(((VertxInternal)vertx).resolveFile(keyPath).getAbsolutePath())); - } - keys.addAll(keyValues); - List certs = new ArrayList<>(); - for (String certPath : certPaths) { - certs.add(vertx.fileSystem().readFileBlocking(((VertxInternal)vertx).resolveFile(certPath).getAbsolutePath())); - } - certs.addAll(certValues); - helper = new KeyStoreHelper(KeyStoreHelper.loadKeyCert(keys, certs), KeyStoreHelper.DUMMY_PASSWORD, null); + X509KeyManager getKeyManager(Vertx vertx) throws Exception { + if (km == null) { + VertxInternal v = (VertxInternal) vertx; + builder = ReloadingKeyStore.Builder.fromPem(v, + certPaths.stream().map(p -> v.resolveFile(p).getAbsolutePath()).collect(Collectors.toList()), + keyPaths.stream().map(p -> v.resolveFile(p).getAbsolutePath()).collect(Collectors.toList()), + certValues, keyValues); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("NewSunX509"); + kmf.init(new KeyStoreBuilderParameters(builder)); + km = (X509KeyManager) kmf.getKeyManagers()[0]; } - return helper; + + return km; } /** @@ -414,19 +418,20 @@ KeyStoreHelper getHelper(Vertx vertx) throws Exception { * @return the {@code KeyStore} */ public KeyStore loadKeyStore(Vertx vertx) throws Exception { - KeyStoreHelper helper = getHelper(vertx); - return helper != null ? helper.store() : null; + // Ensure that KeyStore is constructed. + getKeyManager(vertx); + return builder.getKeyStore(); } @Override public KeyManagerFactory getKeyManagerFactory(Vertx vertx) throws Exception { - KeyStoreHelper helper = getHelper(vertx); - return helper != null ? helper.getKeyMgrFactory() : null; + return new KeyManagerFactoryWrapper(getKeyManager(vertx)); } @Override public Function keyManagerMapper(Vertx vertx) throws Exception { - KeyStoreHelper helper = getHelper(vertx); - return helper != null ? helper::getKeyMgr : null; + X509KeyManager km = getKeyManager(vertx); + // KeyManager will do SNI lookup and mapping from SNI server name to certificate and key alias. + return serverName -> km; } } diff --git a/src/main/java/io/vertx/core/net/impl/DelegatingKeyStoreSpi.java b/src/main/java/io/vertx/core/net/impl/DelegatingKeyStoreSpi.java new file mode 100644 index 00000000000..57d56b33265 --- /dev/null +++ b/src/main/java/io/vertx/core/net/impl/DelegatingKeyStoreSpi.java @@ -0,0 +1,238 @@ +/* +* Copyright (c) 2011-2022 Contributors to the Eclipse Foundation +* +* This program and the accompanying materials are made available under the +* terms of the Eclipse Public License 2.0 which is available at +* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +* which is available at https://www.apache.org/licenses/LICENSE-2.0. +* +* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +*/ + +package io.vertx.core.net.impl; + +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +public abstract class DelegatingKeyStoreSpi extends KeyStoreSpi { + + private static final Logger log = LoggerFactory.getLogger(DelegatingKeyStoreSpi.class); + + private AtomicReference delegate = new AtomicReference<>(); + + // Defines how often the delegate keystore should be checked for updates. + private final Duration cacheTtl = Duration.of(1, ChronoUnit.SECONDS); + + // Defines the next time when to check updates. + private Instant cacheExpiredTime = Instant.MIN; + + + /** + * Reloads the delegate KeyStore if the underlying files have changed on disk. + */ + abstract void refresh() throws Exception; + + + /** + * Calls {@link #refresh()} to refresh the cached KeyStore and if more than {@link #cacheTtl} has passed since last + * refresh. + */ + private void refreshCachedKeyStore() { + // Return if not enough time has passed for the delegate KeyStore to be refreshed. + if (Instant.now().isBefore(cacheExpiredTime)) { + return; + } + + // Set the time when refresh should be checked next. + cacheExpiredTime = Instant.now().plus(cacheTtl); + + try { + refresh(); + } catch (Exception e) { + log.debug("Failed to refresh: " + e); + } + } + + void setKeyStoreDelegate(KeyStore delegate) { + this.delegate.set(new Delegate(delegate)); + } + + @Override + public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException { + refreshCachedKeyStore(); + try { + return delegate.get().keyStore.getKey(alias, password); + } catch (KeyStoreException e) { + log.info("getKey: " + e); + return null; + } + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + refreshCachedKeyStore(); + try { + return delegate.get().keyStore.getCertificateChain(alias); + } catch (KeyStoreException e) { + log.info("getCertificateChain: " + e); + return new Certificate[0]; + } + } + + @Override + public Certificate engineGetCertificate(String alias) { + refreshCachedKeyStore(); + try { + return delegate.get().keyStore.getCertificate(alias); + } catch (KeyStoreException e) { + log.info("getCertificate: " + e); + return null; + } + } + + @Override + public Date engineGetCreationDate(String alias) { + refreshCachedKeyStore(); + try { + return delegate.get().keyStore.getCreationDate(alias); + } catch (KeyStoreException e) { + log.info("getCreationDate: " + e); + return null; + } + } + + @Override + public Enumeration engineAliases() { + refreshCachedKeyStore(); + return Collections.enumeration(new ArrayList<>(delegate.get().sortedAliases)); + } + + @Override + public boolean engineContainsAlias(String alias) { + refreshCachedKeyStore(); + try { + return delegate.get().keyStore.containsAlias(alias); + } catch (KeyStoreException e) { + log.info("containsAlias: " + e); + return false; + } + } + + @Override + public int engineSize() { + refreshCachedKeyStore(); + try { + return delegate.get().keyStore.size(); + } catch (KeyStoreException e) { + log.info("size: " + e); + return 0; + } + } + + @Override + public boolean engineIsKeyEntry(String alias) { + refreshCachedKeyStore(); + try { + return delegate.get().keyStore.isKeyEntry(alias); + } catch (KeyStoreException e) { + log.info("isKeyEntry: " + e); + return false; + } + } + + @Override + public boolean engineIsCertificateEntry(String alias) { + refreshCachedKeyStore(); + try { + return delegate.get().keyStore.isCertificateEntry(alias); + } catch (KeyStoreException e) { + log.info("isCertificateEntry: " + e); + return false; + } + } + + @Override + public String engineGetCertificateAlias(Certificate cert) { + refreshCachedKeyStore(); + try { + return delegate.get().keyStore.getCertificateAlias(cert); + } catch (KeyStoreException e) { + log.info("getCertificateAlias: " + e); + return null; + } + } + + @Override + public void engineLoad(InputStream stream, char[] password) + throws IOException, NoSuchAlgorithmException, CertificateException { + // Nothing to do here since implementations of this class have their own means to load certificates and keys. + } + + private static final String IMMUTABLE_KEYSTORE_ERR = "Modifying keystore is not supported"; + + @Override + public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) throws KeyStoreException { + throw new UnsupportedOperationException(IMMUTABLE_KEYSTORE_ERR); + } + + @Override + public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException { + throw new UnsupportedOperationException(IMMUTABLE_KEYSTORE_ERR); + } + + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { + throw new UnsupportedOperationException(IMMUTABLE_KEYSTORE_ERR); + } + + @Override + public void engineDeleteEntry(String alias) throws KeyStoreException { + throw new UnsupportedOperationException(IMMUTABLE_KEYSTORE_ERR); + } + + @Override + public void engineStore(OutputStream stream, char[] password) + throws IOException, NoSuchAlgorithmException, CertificateException { + throw new UnsupportedOperationException(IMMUTABLE_KEYSTORE_ERR); + } + + class Delegate { + KeyStore keyStore; + List sortedAliases; + + Delegate(KeyStore ks) { + this.keyStore = ks; + + try { + sortedAliases = Collections.list(ks.aliases()); + Collections.sort(sortedAliases); + Collections.reverse(sortedAliases); + } catch (KeyStoreException e) { + // Ignore exception. + log.info("Failed getting aliases" + e); + } + } + } + +} diff --git a/src/main/java/io/vertx/core/net/impl/KeyStoreFileSpi.java b/src/main/java/io/vertx/core/net/impl/KeyStoreFileSpi.java new file mode 100644 index 00000000000..2593decbea7 --- /dev/null +++ b/src/main/java/io/vertx/core/net/impl/KeyStoreFileSpi.java @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2011-2022 Contributors to the Eclipse Foundation +* +* This program and the accompanying materials are made available under the +* terms of the Eclipse Public License 2.0 which is available at +* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +* which is available at https://www.apache.org/licenses/LICENSE-2.0. +* +* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +*/ + +package io.vertx.core.net.impl; + +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.util.function.Supplier; + +import io.vertx.core.VertxException; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.impl.VertxInternal; + +public class KeyStoreFileSpi extends DelegatingKeyStoreSpi { + + private static final Logger log = LoggerFactory.getLogger(KeyStoreFileSpi.class); + + private final VertxInternal vertx; + private final String type; + private final String provider; + private final String path; + private final String password; + private final String alias; + private FileTime lastModified; + + public KeyStoreFileSpi(VertxInternal vertx, String type, String provider, String path, String password, String alias) throws Exception { + if (password == null) { + throw new VertxException("Password must not be null"); + } + + this.vertx = vertx; + this.type = type; + this.provider = provider; + this.path = path; + this.password = password; + this.alias = alias; + + refresh(); + } + + /** + * Reload keystore if it was modified on disk since it was last loaded. + */ + void refresh() throws Exception { + // If keystore has been previously loaded, check the modification timestamp to decide if reload is needed. + if ((lastModified != null) && (lastModified.compareTo(Files.getLastModifiedTime(Paths.get(path))) > 0)) { + // File was not modified since last reload: do nothing. + return; + } + + // Load keystore from disk. + Supplier value; + value = () -> vertx.fileSystem().readFileBlocking(path); + setKeyStoreDelegate(KeyStoreHelper.loadKeyStore(type, provider, password, value, alias)); + this.lastModified = Files.getLastModifiedTime(Paths.get(path)); + } + +} diff --git a/src/main/java/io/vertx/core/net/impl/KeyStoreHelper.java b/src/main/java/io/vertx/core/net/impl/KeyStoreHelper.java index 77c7d411ea9..3c44ea7e5cc 100644 --- a/src/main/java/io/vertx/core/net/impl/KeyStoreHelper.java +++ b/src/main/java/io/vertx/core/net/impl/KeyStoreHelper.java @@ -25,6 +25,7 @@ import java.io.InputStream; import java.net.Socket; import java.security.*; +import java.security.KeyStore.ProtectionParameter; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; diff --git a/src/main/java/io/vertx/core/net/impl/PemFileKeyStoreSpi.java b/src/main/java/io/vertx/core/net/impl/PemFileKeyStoreSpi.java new file mode 100644 index 00000000000..e26d2ea421e --- /dev/null +++ b/src/main/java/io/vertx/core/net/impl/PemFileKeyStoreSpi.java @@ -0,0 +1,130 @@ +/* +* Copyright (c) 2011-2022 Contributors to the Eclipse Foundation +* +* This program and the accompanying materials are made available under the +* terms of the Eclipse Public License 2.0 which is available at +* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +* which is available at https://www.apache.org/licenses/LICENSE-2.0. +* +* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +*/ + +package io.vertx.core.net.impl; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import io.vertx.core.VertxException; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.impl.VertxInternal; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; + +public class PemFileKeyStoreSpi extends DelegatingKeyStoreSpi { + + private static final Logger log = LoggerFactory.getLogger(PemFileKeyStoreSpi.class); + + private final VertxInternal vertx; + private final List fileCredentials = new ArrayList<>(); // Certificates and keys given as paths. + private final List certValues; + private final List keyValues; + + public PemFileKeyStoreSpi(VertxInternal vertx, List certPaths, List keyPaths, + List certValues, List keyValues) throws Exception { + + if ((keyPaths.size() < certPaths.size()) || (keyValues.size() < certValues.size())) { + throw new VertxException("Missing private key"); + } else if ((keyPaths.size() > certPaths.size()) || (keyValues.size() > certValues.size())) { + throw new VertxException("Missing X.509 certificate"); + } else if (keyPaths.isEmpty() && keyValues.isEmpty()) { + throw new VertxException("No credentials configured"); + } + + this.vertx = vertx; + this.certValues = certValues; + this.keyValues = keyValues; + + // Load credentials that were passed as file paths. + Iterator cpi = certPaths.iterator(); + Iterator kpi = keyPaths.iterator(); + while (cpi.hasNext() && kpi.hasNext()) { + fileCredentials.add(new FileCredential(cpi.next(), kpi.next())); + } + + setKeyStoreDelegate(createKeyStore()); + } + + /** + * Reload certificate and key PEM files if they were modified on disk since they were last loaded. + */ + void refresh() throws Exception { + boolean wasReloaded = false; + int i = 0; + for (FileCredential fc : fileCredentials) { + try { + if (fc.needsReload()) { + fileCredentials.set(i, new FileCredential(fc.certPath, fc.keyPath)); + wasReloaded = true; + } + } catch (Exception e) { + log.error("Failed to load: " + e); + } + i++; + } + + // Re-generate KeyStore. + if (wasReloaded) { + setKeyStoreDelegate(createKeyStore()); + } + } + + /** + * Create KeyStore that contains the certificates and keys that were passed by paths and by values. + */ + KeyStore createKeyStore() throws Exception { + List certs = new ArrayList<>(certValues); + List keys = new ArrayList<>(keyValues); + + fileCredentials.stream().forEach(fc -> { + certs.add(fc.certValue); + keys.add(fc.keyValue); + }); + + return KeyStoreHelper.loadKeyCert(keys, certs); + } + + /** + * Holds the content of certificate and key files and their modification timestamps. + */ + class FileCredential { + private final String certPath; + private final String keyPath; + private final FileTime certLastModified; + private final FileTime keyLastModified; + Buffer certValue; + Buffer keyValue; + + FileCredential(String certPath, String keyPath) throws Exception { + this.certPath = certPath; + this.keyPath = keyPath; + + certValue = vertx.fileSystem().readFileBlocking(certPath); + keyValue = vertx.fileSystem().readFileBlocking(keyPath); + + this.certLastModified = Files.getLastModifiedTime(Paths.get(certPath)); + this.keyLastModified = Files.getLastModifiedTime(Paths.get(keyPath)); + } + + boolean needsReload() throws IOException { + return (certLastModified.compareTo(Files.getLastModifiedTime(Paths.get(certPath))) < 0) || + (keyLastModified.compareTo(Files.getLastModifiedTime(Paths.get(keyPath))) < 0); + } + } + +} diff --git a/src/main/java/io/vertx/core/net/impl/ReloadingKeyStore.java b/src/main/java/io/vertx/core/net/impl/ReloadingKeyStore.java new file mode 100644 index 00000000000..e6c824fe4ac --- /dev/null +++ b/src/main/java/io/vertx/core/net/impl/ReloadingKeyStore.java @@ -0,0 +1,97 @@ +/* +* Copyright (c) 2011-2022 Contributors to the Eclipse Foundation +* +* This program and the accompanying materials are made available under the +* terms of the Eclipse Public License 2.0 which is available at +* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +* which is available at https://www.apache.org/licenses/LICENSE-2.0. +* +* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +*/ + +package io.vertx.core.net.impl; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.Objects; + +import io.vertx.core.buffer.Buffer; +import io.vertx.core.impl.VertxInternal; + +/** + * KeyStore that can reload itself when the backing files are modified. + */ +public class ReloadingKeyStore extends KeyStore { + + protected ReloadingKeyStore(KeyStoreSpi keyStoreSpi, Provider provider, String type) + throws NoSuchAlgorithmException, CertificateException, IOException { + super(keyStoreSpi, provider, type); + + // Calling load(), even with null arguments, will initialize the KeyStore to expected state. + load(null, null); + } + + /** + * Builder implementation for reloading keystores. + */ + public static class Builder extends KeyStore.Builder { + + private final KeyStore keyStore; + private final ProtectionParameter protection; + + private final String alias; + private final ProtectionParameter aliasProtection; + + private Builder(KeyStore keyStore, String password, String alias, String keyAliasPassword) { + this.keyStore = keyStore; + this.protection = password != null ? new PasswordProtection(password.toCharArray()) : null; + this.alias = alias; + this.aliasProtection = keyAliasPassword != null ? new PasswordProtection(keyAliasPassword.toCharArray()) : null; + } + + @Override + public KeyStore getKeyStore() { + return keyStore; + } + + @Override + public ProtectionParameter getProtectionParameter(String newSunAlias) { + Objects.requireNonNull(newSunAlias); + + // Parse plain alias from NewSunS509 KeyManager prefixed alias. + // https://github.com/openjdk/jdk/blob/6e55a72f25f7273e3a8a19e0b9a97669b84808e9/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java#L237-L265 + int firstDot = newSunAlias.indexOf('.'); + int secondDot = newSunAlias.indexOf('.', firstDot + 1); + if ((firstDot == -1) || (secondDot == firstDot)) { + // Invalid alias. + return protection; + } + String requestedAlias = newSunAlias.substring(secondDot + 1); + if (requestedAlias.equals(alias) && aliasProtection != null) { + return aliasProtection; + } + return protection; + } + + public static Builder fromKeyStoreFile(VertxInternal vertx, String type, String provider, String path, String password, + String alias, String aliasPassword) throws Exception { + return new Builder(new ReloadingKeyStore( + new KeyStoreFileSpi(vertx, type, provider, path, password, alias), null, "ReloadingKeyStore"), + password, alias, aliasPassword); + } + + public static Builder fromPem(VertxInternal vertx, List certPaths, List keyPaths, + List certValues, List keyValues) throws Exception { + return new Builder(new ReloadingKeyStore( + new PemFileKeyStoreSpi(vertx, certPaths, keyPaths, certValues, keyValues), null, "ReloadingKeyStore"), + KeyStoreHelper.DUMMY_PASSWORD, null, null); + } + + } + +} diff --git a/src/main/java/io/vertx/core/net/impl/SSLHelper.java b/src/main/java/io/vertx/core/net/impl/SSLHelper.java index bdb8f6cd433..7902ba4f2e2 100755 --- a/src/main/java/io/vertx/core/net/impl/SSLHelper.java +++ b/src/main/java/io/vertx/core/net/impl/SSLHelper.java @@ -471,34 +471,16 @@ public SslContext getContext(VertxInternal vertx, String serverName) { public SslContext getContext(VertxInternal vertx, String serverName, boolean useAlpn) { int idx = useAlpn ? 0 : 1; - if (serverName == null) { - if (sslContexts[idx] == null) { - TrustManagerFactory trustMgrFactory; - try { - trustMgrFactory = getTrustMgrFactory(vertx, null); - } catch (Exception e) { - throw new VertxException(e); - } - sslContexts[idx] = createContext(vertx, useAlpn, null, trustMgrFactory); - } - return sslContexts[idx]; - } else { - X509KeyManager mgr; - try { - mgr = keyCertOptions.keyManagerMapper(vertx).apply(serverName); - } catch (Exception e) { - throw new RuntimeException(e); - } - if (mgr == null) { - return sslContexts[idx]; - } + if (sslContexts[idx] == null) { + TrustManagerFactory trustMgrFactory; try { - TrustManagerFactory trustMgrFactory = getTrustMgrFactory(vertx, serverName); - return sslContextMaps[idx].computeIfAbsent(mgr.getCertificateChain(null)[0], s -> createContext(vertx, useAlpn, mgr, trustMgrFactory)); + trustMgrFactory = getTrustMgrFactory(vertx, serverName); } catch (Exception e) { throw new VertxException(e); } + sslContexts[idx] = createContext(vertx, useAlpn, null, trustMgrFactory); } + return sslContexts[idx]; } // This is called to validate some of the SSL params as that only happens when the context is created diff --git a/src/test/java/io/vertx/core/http/HttpTLSTest.java b/src/test/java/io/vertx/core/http/HttpTLSTest.java index 6e1f63e1cff..812d2d0cfd9 100755 --- a/src/test/java/io/vertx/core/http/HttpTLSTest.java +++ b/src/test/java/io/vertx/core/http/HttpTLSTest.java @@ -466,22 +466,30 @@ public void testSNITrustPEM() throws Exception { } @Test - // Client provides SNI but server ignores it and provides a different cerficate + // Client provides SNI when server option setSni(false). + // Server still processes SNI and returns correct certificate. + // NOTE: It is not possible to disable SNI support in NewSunX509 KeyManager. The impact is following: + // - CORRECT server certificate is returned according to requested SNI, even if server option setSni(false). + // - Previously, before switching to SNI handling in NewSunX509 KeyManager, INCORRECT certificate was returned. public void testSNIServerIgnoresExtension1() throws Exception { testTLS(Cert.NONE, Trust.SNI_JKS_HOST2, Cert.SNI_JKS, Trust.NONE) .requestOptions(new RequestOptions().setSsl(true).setPort(4043).setHost("host2.com")) - .fail(); + .pass(); } @Test - // Client provides SNI but server ignores it and provides a different cerficate - check we get a certificate + // Client provides SNI when server option setSni(false). + // Server still processes SNI and returns correct certificate. + // NOTE: It is not possible to disable SNI support in NewSunX509 KeyManager. The impact is following: + // - CORRECT server certificate is returned according to requested SNI, even if server option setSni(false). + // - Previously, before switching to SNI handling in NewSunX509 KeyManager, INCORRECT certificate was returned. public void testSNIServerIgnoresExtension2() throws Exception { - Certificate cert = testTLS(Cert.NONE, Trust.SERVER_JKS, Cert.SNI_JKS, Trust.NONE) + Certificate cert = testTLS(Cert.NONE, Trust.SNI_JKS_HOST2, Cert.SNI_JKS, Trust.NONE) .clientVerifyHost(false) .requestOptions(new RequestOptions().setSsl(true).setPort(4043).setHost("host2.com")) .pass() .clientPeerCert(); - assertEquals("localhost", TestUtils.cnOf(cert)); + assertEquals("host2.com", TestUtils.cnOf(cert)); } @Test @@ -655,25 +663,31 @@ public void testSNISubjectAltenativeNameCNMatch1PEM() throws Exception { } @Test + // Client sets SNI servername to host5.com. + // Server certificate has Subject CN=host5.com but SAN DNS *.host5.com. + // Default server certificate CN=localhost is returned because host5.com did not *.host5.com. public void testSNISubjectAltenativeNameCNMatch2() throws Exception { - Certificate cert = testTLS(Cert.NONE, Trust.SNI_JKS_HOST5, Cert.SNI_JKS, Trust.NONE) + Certificate cert = testTLS(Cert.NONE, Trust.SERVER_JKS, Cert.SNI_JKS, Trust.NONE) .serverSni() .clientVerifyHost(false) .requestOptions(new RequestOptions().setSsl(true).setPort(4043).setHost("host5.com")) .pass() .clientPeerCert(); - assertEquals("host5.com", TestUtils.cnOf(cert)); + assertEquals("localhost", TestUtils.cnOf(cert)); } @Test + // Client sets SNI servername to host5.com. + // Server certificate has Subject CN=host5.com but SAN DNS *.host5.com. + // Default server certificate CN=localhost is returned because host5.com did not *.host5.com. public void testSNISubjectAltenativeNameCNMatch2PKCS12() throws Exception { - Certificate cert = testTLS(Cert.NONE, Trust.SNI_JKS_HOST5, Cert.SNI_PKCS12, Trust.NONE) + Certificate cert = testTLS(Cert.NONE, Trust.SERVER_JKS, Cert.SNI_PKCS12, Trust.NONE) .serverSni() .clientVerifyHost(false) .requestOptions(new RequestOptions().setSsl(true).setPort(4043).setHost("host5.com")) .pass() .clientPeerCert(); - assertEquals("host5.com", TestUtils.cnOf(cert)); + assertEquals("localhost", TestUtils.cnOf(cert)); } @Test @@ -790,13 +804,15 @@ public void testSNIWithServerNameTrustFallbackFail() throws Exception { } @Test + // Server requires client certificate which is issued by either Root CA or Other CA. + // Client provides certificate issued by Root CA. public void testSNIWithServerNameTrustFail() throws Exception { testTLS(Cert.CLIENT_PEM_ROOT_CA, Trust.SNI_JKS_HOST2, Cert.SNI_JKS, Trust.SNI_SERVER_ROOT_CA_AND_OTHER_CA_2).serverSni() .requestOptions(new RequestOptions().setSsl(true) .setPort(4043) .setHost("host2.com")) .requiresClientAuth() - .fail(); + .pass(); } @Test @@ -1321,13 +1337,7 @@ public void testPKCS12InvalidPath() { @Test public void testPKCS12MissingPassword() { - String msg; - if (PlatformDependent.javaVersion() < 15) { - msg = "Get Key failed: null"; - } else { - msg = "Get Key failed: Cannot read the array length because \"password\" is null"; - } - testInvalidKeyStore(Cert.SERVER_PKCS12.get().setPassword(null), msg, null); + testInvalidKeyStore(Cert.SERVER_PKCS12.get().setPassword(null), "Password must not be null", null); } @Test diff --git a/src/test/java/io/vertx/core/net/KeyStoreHelperTest.java b/src/test/java/io/vertx/core/net/KeyStoreHelperTest.java index c7ac4612b83..e720f7c79a5 100644 --- a/src/test/java/io/vertx/core/net/KeyStoreHelperTest.java +++ b/src/test/java/io/vertx/core/net/KeyStoreHelperTest.java @@ -18,11 +18,14 @@ import java.security.KeyFactory; import java.security.KeyStore; import java.security.KeyStoreException; +import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.RSAPrivateKey; import java.util.Enumeration; +import javax.net.ssl.X509KeyManager; + import io.vertx.core.net.impl.KeyStoreHelper; import io.vertx.test.core.VertxTestBase; import org.junit.Assume; @@ -49,8 +52,7 @@ public void testKeyStoreHelperSupportsRSAPrivateKeys() throws Exception { PemKeyCertOptions options = new PemKeyCertOptions() .addKeyPath("target/test-classes/tls/server-key.pem") .addCertPath("target/test-classes/tls/server-cert.pem"); - KeyStoreHelper helper = options.getHelper(vertx); - assertKeyType(helper.store(), RSAPrivateKey.class); + assertKeyType(options.loadKeyStore(vertx), RSAPrivateKey.class); } /** @@ -66,8 +68,7 @@ public void testKeyStoreHelperSupportsECPrivateKeys() throws Exception { PemKeyCertOptions options = new PemKeyCertOptions() .addKeyPath("target/test-classes/tls/server-key-ec.pem") .addCertPath("target/test-classes/tls/server-cert-ec.pem"); - KeyStoreHelper helper = options.getHelper(vertx); - assertKeyType(helper.store(), ECPrivateKey.class); + assertKeyType(options.loadKeyStore(vertx), ECPrivateKey.class); } private void assertKeyType(KeyStore store, Class expectedKeyType) throws KeyStoreException, GeneralSecurityException { diff --git a/src/test/java/io/vertx/core/net/KeyStoreTest.java b/src/test/java/io/vertx/core/net/KeyStoreTest.java index c8265143e0a..7f82629dc7b 100644 --- a/src/test/java/io/vertx/core/net/KeyStoreTest.java +++ b/src/test/java/io/vertx/core/net/KeyStoreTest.java @@ -23,7 +23,10 @@ import org.junit.Test; import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; + import java.security.KeyStore; import java.util.Arrays; import java.util.Collections; @@ -350,7 +353,7 @@ public void testCopyTrustOptions() throws Exception { @Test public void testJKSPath() throws Exception { - testKeyStore(Cert.SERVER_JKS.get().getHelper(vertx)); + testKeyStoreOptions(Cert.SERVER_JKS.get()); } @Test @@ -358,7 +361,7 @@ public void testJKSValue() throws Exception { JksOptions options = Cert.SERVER_JKS.get(); Buffer store = vertx.fileSystem().readFileBlocking(options.getPath()); options.setPath(null).setValue(store); - testKeyStore(options.getHelper(vertx)); + testKeyStoreOptions(options); } @Test @@ -373,12 +376,12 @@ public void testKeyStoreValue() throws Exception { .setPath(null) .setValue(store) .setPassword(jks.getPassword()); - testKeyStore(options.getHelper(vertx)); + testKeyStoreOptions(options); } @Test public void testPKCS12Path() throws Exception { - testKeyStore(Cert.SERVER_PKCS12.get().getHelper(vertx)); + testKeyStoreOptions(Cert.SERVER_PKCS12.get()); } @Test @@ -386,12 +389,12 @@ public void testPKCS12Value() throws Exception { PfxOptions options = Cert.SERVER_PKCS12.get(); Buffer store = vertx.fileSystem().readFileBlocking(options.getPath()); options.setPath(null).setValue(store); - testKeyStore(options.getHelper(vertx)); + testKeyStoreOptions(options); } @Test public void testKeyCertPath() throws Exception { - testKeyStore(Cert.SERVER_PEM.get().getHelper(vertx)); + testPemKeyCertOptions(Cert.SERVER_PEM.get()); } /** @@ -400,7 +403,7 @@ public void testKeyCertPath() throws Exception { */ @Test public void testRsaKeyCertPath() throws Exception { - testKeyStore(Cert.SERVER_PEM_RSA.get().getHelper(vertx)); + testPemKeyCertOptions(Cert.SERVER_PEM_RSA.get()); } @Test @@ -410,7 +413,7 @@ public void testKeyCertValue() throws Exception { options.setKeyValue(null).setKeyValue(key); Buffer cert = vertx.fileSystem().readFileBlocking(options.getCertPath()); options.setCertValue(null).setCertValue(cert); - testKeyStore(options.getHelper(vertx)); + testPemKeyCertOptions(options); } @Test @@ -455,6 +458,20 @@ private void testKeyStore(KeyStoreHelper helper) throws Exception { assertTrue(keyManagers.length > 0); } + private void testPemKeyCertOptions(PemKeyCertOptions options) throws Exception { + Enumeration aliases = options.loadKeyStore(vertx).aliases(); + assertTrue(aliases.hasMoreElements()); + KeyManager[] keyManagers = options.getKeyManagerFactory(vertx).getKeyManagers(); + assertTrue(keyManagers.length > 0); + } + + private void testKeyStoreOptions(KeyStoreOptionsBase options) throws Exception { + Enumeration aliases = options.loadKeyStore(vertx).aliases(); + assertTrue(aliases.hasMoreElements()); + KeyManager[] keyManagers = options.getKeyManagerFactory(vertx).getKeyManagers(); + assertTrue(keyManagers.length > 0); + } + private void testTrustStore(KeyStoreHelper helper) throws Exception { TrustManager[] keyManagers = helper.getTrustMgrs((VertxInternal) vertx); assertTrue(keyManagers.length > 0); diff --git a/src/test/java/io/vertx/core/net/NetTest.java b/src/test/java/io/vertx/core/net/NetTest.java index fda0976f20c..ab9a4c4b380 100755 --- a/src/test/java/io/vertx/core/net/NetTest.java +++ b/src/test/java/io/vertx/core/net/NetTest.java @@ -1569,6 +1569,8 @@ public void testSniWithServerNameTrustFallbackFail(){ } @Test + // Server requires client certificate which is issued by either Root CA or Other CA. + // Client provides certificate issued by Root CA. public void testSniWithServerNameTrustFail(){ TLSTest test = new TLSTest().clientTrust(Trust.SNI_JKS_HOST2) .clientCert(Cert.CLIENT_PEM_ROOT_CA) @@ -1577,7 +1579,7 @@ public void testSniWithServerNameTrustFail(){ .sni(true) .serverName("host2.com") .serverTrust(Trust.SNI_SERVER_ROOT_CA_AND_OTHER_CA_2); - test.run(false); + test.run(true); await(); }