From 0ede28481c8dc092c4c7f9bb9c1c678d6536a285 Mon Sep 17 00:00:00 2001 From: Saranya Krishnakumar Date: Mon, 17 Oct 2022 14:12:46 -0700 Subject: [PATCH] Allow hot reloading of certs through KeyCertOptions Co-authored-by: Francisco Guerrero --- .../vertx/core/net/JksOptionsConverter.java | 16 +++ .../core/net/KeyStoreOptionsConverter.java | 16 +++ .../core/net/PemKeyCertOptionsConverter.java | 16 +++ .../vertx/core/net/PfxOptionsConverter.java | 16 +++ .../vertx/core/http/impl/HttpServerImpl.java | 13 +- .../java/io/vertx/core/net/JksOptions.java | 10 ++ .../io/vertx/core/net/KeyCertOptions.java | 31 +++++ .../io/vertx/core/net/KeyStoreOptions.java | 10 ++ .../vertx/core/net/KeyStoreOptionsBase.java | 71 ++++++++++ .../io/vertx/core/net/PemKeyCertOptions.java | 113 ++++++++++++++++ .../java/io/vertx/core/net/PfxOptions.java | 10 ++ .../io/vertx/core/net/impl/SSLHelper.java | 10 ++ .../io/vertx/core/net/impl/TCPServerBase.java | 26 +++- .../core/net/SSLContextReloaderTest.java | 128 ++++++++++++++++++ .../resources/tls/client-truststore-2.p12 | Bin 0 -> 1186 bytes src/test/resources/tls/server-keystore-2.p12 | Bin 0 -> 2493 bytes 16 files changed, 480 insertions(+), 6 deletions(-) create mode 100644 src/test/java/io/vertx/core/net/SSLContextReloaderTest.java create mode 100644 src/test/resources/tls/client-truststore-2.p12 create mode 100644 src/test/resources/tls/server-keystore-2.p12 diff --git a/src/main/generated/io/vertx/core/net/JksOptionsConverter.java b/src/main/generated/io/vertx/core/net/JksOptionsConverter.java index 20fbee73c96..78e35491295 100644 --- a/src/main/generated/io/vertx/core/net/JksOptionsConverter.java +++ b/src/main/generated/io/vertx/core/net/JksOptionsConverter.java @@ -30,6 +30,11 @@ public static void fromJson(Iterable> json, obj.setAliasPassword((String)member.getValue()); } break; + case "certRefreshRateInSeconds": + if (member.getValue() instanceof Number) { + obj.setCertRefreshRateInSeconds(((Number)member.getValue()).longValue()); + } + break; case "password": if (member.getValue() instanceof String) { obj.setPassword((String)member.getValue()); @@ -40,6 +45,11 @@ public static void fromJson(Iterable> json, obj.setPath((String)member.getValue()); } break; + case "reloadCerts": + if (member.getValue() instanceof Boolean) { + obj.setReloadCerts((Boolean)member.getValue()); + } + break; case "value": if (member.getValue() instanceof String) { obj.setValue(io.vertx.core.buffer.Buffer.buffer(BASE64_DECODER.decode((String)member.getValue()))); @@ -60,12 +70,18 @@ public static void toJson(JksOptions obj, java.util.Map json) { if (obj.getAliasPassword() != null) { json.put("aliasPassword", obj.getAliasPassword()); } + if (obj.getCertRefreshRateInSeconds() != null) { + json.put("certRefreshRateInSeconds", obj.getCertRefreshRateInSeconds()); + } if (obj.getPassword() != null) { json.put("password", obj.getPassword()); } if (obj.getPath() != null) { json.put("path", obj.getPath()); } + if (obj.getReloadCerts() != null) { + json.put("reloadCerts", obj.getReloadCerts()); + } if (obj.getValue() != null) { json.put("value", BASE64_ENCODER.encodeToString(obj.getValue().getBytes())); } diff --git a/src/main/generated/io/vertx/core/net/KeyStoreOptionsConverter.java b/src/main/generated/io/vertx/core/net/KeyStoreOptionsConverter.java index 0af21906eb6..7d04c208e11 100644 --- a/src/main/generated/io/vertx/core/net/KeyStoreOptionsConverter.java +++ b/src/main/generated/io/vertx/core/net/KeyStoreOptionsConverter.java @@ -30,6 +30,11 @@ public static void fromJson(Iterable> json, obj.setAliasPassword((String)member.getValue()); } break; + case "certRefreshRateInSeconds": + if (member.getValue() instanceof Number) { + obj.setCertRefreshRateInSeconds(((Number)member.getValue()).longValue()); + } + break; case "password": if (member.getValue() instanceof String) { obj.setPassword((String)member.getValue()); @@ -45,6 +50,11 @@ public static void fromJson(Iterable> json, obj.setProvider((String)member.getValue()); } break; + case "reloadCerts": + if (member.getValue() instanceof Boolean) { + obj.setReloadCerts((Boolean)member.getValue()); + } + break; case "type": if (member.getValue() instanceof String) { obj.setType((String)member.getValue()); @@ -70,6 +80,9 @@ public static void toJson(KeyStoreOptions obj, java.util.Map jso if (obj.getAliasPassword() != null) { json.put("aliasPassword", obj.getAliasPassword()); } + if (obj.getCertRefreshRateInSeconds() != null) { + json.put("certRefreshRateInSeconds", obj.getCertRefreshRateInSeconds()); + } if (obj.getPassword() != null) { json.put("password", obj.getPassword()); } @@ -79,6 +92,9 @@ public static void toJson(KeyStoreOptions obj, java.util.Map jso if (obj.getProvider() != null) { json.put("provider", obj.getProvider()); } + if (obj.getReloadCerts() != null) { + json.put("reloadCerts", obj.getReloadCerts()); + } if (obj.getType() != null) { json.put("type", obj.getType()); } diff --git a/src/main/generated/io/vertx/core/net/PemKeyCertOptionsConverter.java b/src/main/generated/io/vertx/core/net/PemKeyCertOptionsConverter.java index b2d36deed5f..a27aacc7618 100644 --- a/src/main/generated/io/vertx/core/net/PemKeyCertOptionsConverter.java +++ b/src/main/generated/io/vertx/core/net/PemKeyCertOptionsConverter.java @@ -35,6 +35,11 @@ static void fromJson(Iterable> json, PemKeyC obj.setCertPaths(list); } break; + case "certRefreshRateInSeconds": + if (member.getValue() instanceof Number) { + obj.setCertRefreshRateInSeconds(((Number)member.getValue()).longValue()); + } + break; case "certValue": if (member.getValue() instanceof String) { obj.setCertValue(io.vertx.core.buffer.Buffer.buffer(BASE64_DECODER.decode((String)member.getValue()))); @@ -80,6 +85,11 @@ static void fromJson(Iterable> json, PemKeyC obj.setKeyValues(list); } break; + case "reloadCerts": + if (member.getValue() instanceof Boolean) { + obj.setReloadCerts((Boolean)member.getValue()); + } + break; } } } @@ -94,6 +104,9 @@ static void toJson(PemKeyCertOptions obj, java.util.Map json) { obj.getCertPaths().forEach(item -> array.add(item)); json.put("certPaths", array); } + if (obj.getCertRefreshRateInSeconds() != null) { + json.put("certRefreshRateInSeconds", obj.getCertRefreshRateInSeconds()); + } if (obj.getCertValues() != null) { JsonArray array = new JsonArray(); obj.getCertValues().forEach(item -> array.add(BASE64_ENCODER.encodeToString(item.getBytes()))); @@ -109,5 +122,8 @@ static void toJson(PemKeyCertOptions obj, java.util.Map json) { obj.getKeyValues().forEach(item -> array.add(BASE64_ENCODER.encodeToString(item.getBytes()))); json.put("keyValues", array); } + if (obj.getReloadCerts() != null) { + json.put("reloadCerts", obj.getReloadCerts()); + } } } diff --git a/src/main/generated/io/vertx/core/net/PfxOptionsConverter.java b/src/main/generated/io/vertx/core/net/PfxOptionsConverter.java index 96c6a009d87..7b1616f01f1 100644 --- a/src/main/generated/io/vertx/core/net/PfxOptionsConverter.java +++ b/src/main/generated/io/vertx/core/net/PfxOptionsConverter.java @@ -30,6 +30,11 @@ static void fromJson(Iterable> json, PfxOpti obj.setAliasPassword((String)member.getValue()); } break; + case "certRefreshRateInSeconds": + if (member.getValue() instanceof Number) { + obj.setCertRefreshRateInSeconds(((Number)member.getValue()).longValue()); + } + break; case "password": if (member.getValue() instanceof String) { obj.setPassword((String)member.getValue()); @@ -40,6 +45,11 @@ static void fromJson(Iterable> json, PfxOpti obj.setPath((String)member.getValue()); } break; + case "reloadCerts": + if (member.getValue() instanceof Boolean) { + obj.setReloadCerts((Boolean)member.getValue()); + } + break; case "value": if (member.getValue() instanceof String) { obj.setValue(io.vertx.core.buffer.Buffer.buffer(BASE64_DECODER.decode((String)member.getValue()))); @@ -60,12 +70,18 @@ static void toJson(PfxOptions obj, java.util.Map json) { if (obj.getAliasPassword() != null) { json.put("aliasPassword", obj.getAliasPassword()); } + if (obj.getCertRefreshRateInSeconds() != null) { + json.put("certRefreshRateInSeconds", obj.getCertRefreshRateInSeconds()); + } if (obj.getPassword() != null) { json.put("password", obj.getPassword()); } if (obj.getPath() != null) { json.put("path", obj.getPath()); } + if (obj.getReloadCerts() != null) { + json.put("reloadCerts", obj.getReloadCerts()); + } if (obj.getValue() != null) { json.put("value", BASE64_ENCODER.encodeToString(obj.getValue().getBytes())); } diff --git a/src/main/java/io/vertx/core/http/impl/HttpServerImpl.java b/src/main/java/io/vertx/core/http/impl/HttpServerImpl.java index 7d379cd6dda..ae28ee63b54 100644 --- a/src/main/java/io/vertx/core/http/impl/HttpServerImpl.java +++ b/src/main/java/io/vertx/core/http/impl/HttpServerImpl.java @@ -21,6 +21,7 @@ import io.vertx.core.impl.VertxInternal; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.net.KeyCertOptions; import io.vertx.core.net.SocketAddress; import io.vertx.core.net.impl.*; import io.vertx.core.spi.metrics.MetricsProvider; @@ -200,11 +201,13 @@ protected Handler childHandler(ContextInternal context, SocketAddress a @Override protected SSLHelper createSSLHelper() { - return new SSLHelper(options, options - .getAlpnVersions() - .stream() - .map(HttpVersion::alpnName) - .collect(Collectors.toList())); + KeyCertOptions keyCertOptions = options.getKeyCertOptions(); + SSLHelper sslHelper = new SSLHelper(options, options.getAlpnVersions() + .stream() + .map(HttpVersion::alpnName) + .collect(Collectors.toList())); + startPeriodicSSLContextReload(keyCertOptions, sslHelper); + return sslHelper; } @Override diff --git a/src/main/java/io/vertx/core/net/JksOptions.java b/src/main/java/io/vertx/core/net/JksOptions.java index 7272bbd76e3..455261b658b 100644 --- a/src/main/java/io/vertx/core/net/JksOptions.java +++ b/src/main/java/io/vertx/core/net/JksOptions.java @@ -82,6 +82,16 @@ public JksOptions setAliasPassword(String aliasPassword) { return (JksOptions) super.setAliasPassword(aliasPassword); } + @Override + public JksOptions setReloadCerts(Boolean reloadCerts) { + return (JksOptions) super.setReloadCerts(reloadCerts); + } + + @Override + public JksOptions setCertRefreshRateInSeconds(Long certRefreshRateInSeconds) { + return (JksOptions) super.setCertRefreshRateInSeconds(certRefreshRateInSeconds); + } + @Override public JksOptions copy() { return new JksOptions(this); diff --git a/src/main/java/io/vertx/core/net/KeyCertOptions.java b/src/main/java/io/vertx/core/net/KeyCertOptions.java index 05350761b70..416e45bd860 100644 --- a/src/main/java/io/vertx/core/net/KeyCertOptions.java +++ b/src/main/java/io/vertx/core/net/KeyCertOptions.java @@ -11,10 +11,12 @@ package io.vertx.core.net; +import io.vertx.codegen.annotations.GenIgnore; import io.vertx.core.Vertx; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.X509KeyManager; +import java.util.concurrent.TimeUnit; import java.util.function.Function; /** @@ -39,6 +41,35 @@ public interface KeyCertOptions { */ KeyManagerFactory getKeyManagerFactory(Vertx vertx) throws Exception; + /** + * This method is used to check if context reloading is enabled. + */ + default Boolean getReloadCerts() { + return Boolean.FALSE; + } + + /** + * This method is used by {@link io.vertx.core.net.impl.SSLHelper} to check if context reloading is needed. + */ + @GenIgnore(GenIgnore.PERMITTED_TYPE) + default boolean isReloadNeeded() { + return false; + } + + /** + * This method returns default certificate refresh rate. By default, set to 5 hrs. + */ + default Long getCertRefreshRateInSeconds() { + return TimeUnit.HOURS.toSeconds(5); + } + + /** + * This method is used by {@link io.vertx.core.net.impl.SSLHelper} for reloading certs present in cert/keystore paths. + */ + default void reload() { + throw new UnsupportedOperationException("Default reload no-op method called"); + } + /** * Returns a function that maps SNI server names to {@link X509KeyManager} instance. * diff --git a/src/main/java/io/vertx/core/net/KeyStoreOptions.java b/src/main/java/io/vertx/core/net/KeyStoreOptions.java index 192648aad53..a9a0a8cb278 100644 --- a/src/main/java/io/vertx/core/net/KeyStoreOptions.java +++ b/src/main/java/io/vertx/core/net/KeyStoreOptions.java @@ -136,6 +136,16 @@ public KeyStoreOptions setAliasPassword(String aliasPassword) { return (KeyStoreOptions) super.setAliasPassword(aliasPassword); } + @Override + public KeyStoreOptions setReloadCerts(Boolean reloadCerts) { + return (KeyStoreOptions) super.setReloadCerts(reloadCerts); + } + + @Override + public KeyStoreOptions setCertRefreshRateInSeconds(Long certRefreshRateInSeconds) { + return (KeyStoreOptions) super.setCertRefreshRateInSeconds(certRefreshRateInSeconds); + } + @Override public KeyStoreOptions copy() { return new KeyStoreOptions(this); diff --git a/src/main/java/io/vertx/core/net/KeyStoreOptionsBase.java b/src/main/java/io/vertx/core/net/KeyStoreOptionsBase.java index 16907ad8b07..6fb65fc9c51 100644 --- a/src/main/java/io/vertx/core/net/KeyStoreOptionsBase.java +++ b/src/main/java/io/vertx/core/net/KeyStoreOptionsBase.java @@ -11,6 +11,7 @@ package io.vertx.core.net; +import io.vertx.codegen.annotations.GenIgnore; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.impl.VertxInternal; @@ -20,6 +21,9 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509KeyManager; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.KeyStore; import java.util.function.Function; import java.util.function.Supplier; @@ -40,6 +44,9 @@ public abstract class KeyStoreOptionsBase implements KeyCertOptions, TrustOption private Buffer value; private String alias; private String aliasPassword; + private Boolean reloadCerts; + private Long certRefreshRateInSeconds; + private long keystoreLastModifiedTimestamp; /** * Default constructor @@ -61,6 +68,8 @@ protected KeyStoreOptionsBase(KeyStoreOptionsBase other) { this.value = other.value; this.alias = other.alias; this.aliasPassword = other.aliasPassword; + this.reloadCerts = other.reloadCerts; + this.certRefreshRateInSeconds = other.certRefreshRateInSeconds; } protected String getType() { @@ -108,6 +117,62 @@ public String getPath() { return path; } + /** + * Set whether certificates should be reloaded from their path. + * + * @return a reference to this, so the API can be used fluently + */ + public KeyStoreOptionsBase setReloadCerts(Boolean reloadCerts) { + this.reloadCerts = reloadCerts; + return this; + } + + /** + * Set certificate refresh rate in seconds. + * + * @return a reference to this, so the API can be used fluently + */ + public KeyStoreOptionsBase setCertRefreshRateInSeconds(Long certRefreshRateInSeconds) { + this.certRefreshRateInSeconds = certRefreshRateInSeconds; + return this; + } + + /** + * @return certificate refresh rate. + */ + public Long getCertRefreshRateInSeconds() { + return this.certRefreshRateInSeconds; + } + + /** + * This method is used to check if context reloading is enabled. + */ + public Boolean getReloadCerts() { + return this.reloadCerts; + } + + /** + * @return {@code boolean} indicating whether certificates should be reloaded or not. + */ + @Override + @GenIgnore(GenIgnore.PERMITTED_TYPE) + public boolean isReloadNeeded() { + long previousTimestamp = this.keystoreLastModifiedTimestamp; + this.keystoreLastModifiedTimestamp = getLastModifiedTimestamp(this.path); + return previousTimestamp != this.keystoreLastModifiedTimestamp; + } + + private long getLastModifiedTimestamp(String path) { + if (path == null) { + return 0; + } + try { + return Files.getLastModifiedTime(Paths.get(path)).toMillis(); + } catch (IOException e) { + return 0; + } + } + /** * Set the path to the key store * @@ -116,6 +181,7 @@ public String getPath() { */ public KeyStoreOptionsBase setPath(String path) { this.path = path; + this.keystoreLastModifiedTimestamp = getLastModifiedTimestamp(path); return this; } @@ -189,6 +255,11 @@ KeyStoreHelper getHelper(Vertx vertx) throws Exception { return helper; } + @Override + public void reload() { + this.helper = null; + } + /** * Load and return a Java keystore. * diff --git a/src/main/java/io/vertx/core/net/PemKeyCertOptions.java b/src/main/java/io/vertx/core/net/PemKeyCertOptions.java index 681010355a0..cd772f1de4f 100644 --- a/src/main/java/io/vertx/core/net/PemKeyCertOptions.java +++ b/src/main/java/io/vertx/core/net/PemKeyCertOptions.java @@ -22,9 +22,14 @@ import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.X509KeyManager; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.KeyStore; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Function; /** @@ -102,6 +107,10 @@ public class PemKeyCertOptions implements KeyCertOptions { private List keyValues; private List certPaths; private List certValues; + private Boolean reloadCerts; + private Long certRefreshRateInSeconds; + private Map keyFilesLastModifiedTimestamps; + private Map certFilesLastModifiedTimestamps; /** * Default constructor @@ -116,6 +125,8 @@ private void init() { keyValues = new ArrayList<>(); certPaths = new ArrayList<>(); certValues = new ArrayList<>(); + keyFilesLastModifiedTimestamps = new HashMap<>(); + certFilesLastModifiedTimestamps = new HashMap<>(); } /** @@ -129,6 +140,14 @@ public PemKeyCertOptions(PemKeyCertOptions other) { this.keyValues = other.keyValues != null ? new ArrayList<>(other.keyValues) : new ArrayList<>(); this.certPaths = other.certPaths != null ? new ArrayList<>(other.certPaths) : new ArrayList<>(); this.certValues = other.certValues != null ? new ArrayList<>(other.certValues) : new ArrayList<>(); + this.reloadCerts = other.reloadCerts; + this.certRefreshRateInSeconds = other.certRefreshRateInSeconds; + this.keyFilesLastModifiedTimestamps = other.keyFilesLastModifiedTimestamps != null + ? new HashMap<>(other.keyFilesLastModifiedTimestamps) + : new HashMap<>(); + this.certFilesLastModifiedTimestamps = other.certFilesLastModifiedTimestamps != null + ? new HashMap<>(other.certFilesLastModifiedTimestamps) + : new HashMap<>(); } /** @@ -171,8 +190,11 @@ public String getKeyPath() { */ public PemKeyCertOptions setKeyPath(String keyPath) { keyPaths.clear(); + keyFilesLastModifiedTimestamps.clear(); if (keyPath != null) { keyPaths.add(keyPath); + long recentLastModifiedTimestamp = getLastModifiedTimestamp(keyPath); + keyFilesLastModifiedTimestamps.put(keyPath, recentLastModifiedTimestamp); } return this; } @@ -194,7 +216,12 @@ public List getKeyPaths() { */ public PemKeyCertOptions setKeyPaths(List keyPaths) { this.keyPaths.clear(); + this.keyFilesLastModifiedTimestamps.clear(); this.keyPaths.addAll(keyPaths); + keyPaths.forEach(keyPath -> { + long recentLastModifiedTimestamp = getLastModifiedTimestamp(keyPath); + this.keyFilesLastModifiedTimestamps.put(keyPath, recentLastModifiedTimestamp); + }); return this; } @@ -208,6 +235,8 @@ public PemKeyCertOptions setKeyPaths(List keyPaths) { public PemKeyCertOptions addKeyPath(String keyPath) { Arguments.require(keyPath != null, "Null keyPath"); keyPaths.add(keyPath); + long recentLastModifiedTimestamp = getLastModifiedTimestamp(keyPath); + keyFilesLastModifiedTimestamps.put(keyPath, recentLastModifiedTimestamp); return this; } @@ -287,8 +316,11 @@ public String getCertPath() { */ public PemKeyCertOptions setCertPath(String certPath) { certPaths.clear(); + certFilesLastModifiedTimestamps.clear(); if (certPath != null) { certPaths.add(certPath); + long recentLastModifiedTimestamp = getLastModifiedTimestamp(certPath); + certFilesLastModifiedTimestamps.put(certPath, recentLastModifiedTimestamp); } return this; } @@ -310,7 +342,12 @@ public List getCertPaths() { */ public PemKeyCertOptions setCertPaths(List certPaths) { this.certPaths.clear(); + this.certFilesLastModifiedTimestamps.clear(); this.certPaths.addAll(certPaths); + certPaths.forEach(certPath -> { + long recentLastModifiedTimestamp = getLastModifiedTimestamp(certPath); + this.certFilesLastModifiedTimestamps.put(certPath, recentLastModifiedTimestamp); + }); return this; } @@ -324,9 +361,22 @@ public PemKeyCertOptions setCertPaths(List certPaths) { public PemKeyCertOptions addCertPath(String certPath) { Arguments.require(certPath != null, "Null certPath"); certPaths.add(certPath); + long recentLastModifiedTimestamp = getLastModifiedTimestamp(certPath); + certFilesLastModifiedTimestamps.put(certPath, recentLastModifiedTimestamp); return this; } + private long getLastModifiedTimestamp(String path) { + if (path == null) { + return 0; + } + try { + return Files.getLastModifiedTime(Paths.get(path)).toMillis(); + } catch (IOException e) { + return 0; + } + } + /** * Get the first certificate as a buffer * @@ -385,6 +435,64 @@ public PemKeyCertOptions addCertValue(Buffer certValue) { return this; } + /** + * Set whether certificates should be reloaded from their path. + * + * @return a reference to this, so the API can be used fluently + */ + public PemKeyCertOptions setReloadCerts(Boolean reloadCerts) { + this.reloadCerts = reloadCerts; + return this; + } + + /** + * Set certificate refresh rate in seconds. + * + * @return a reference to this, so the API can be used fluently + */ + public PemKeyCertOptions setCertRefreshRateInSeconds(Long certRefreshRateInSeconds) { + this.certRefreshRateInSeconds = certRefreshRateInSeconds; + return this; + } + + /** + * @return certificate refresh rate. + */ + public Long getCertRefreshRateInSeconds() { + return this.certRefreshRateInSeconds; + } + + /** + * This method is used to check if context reloading is enabled. + */ + public Boolean getReloadCerts() { + return this.reloadCerts; + } + + /** + * @return {@code boolean} indicating whether certificates/keys should be reloaded or not. + */ + @Override + @GenIgnore(GenIgnore.PERMITTED_TYPE) + public boolean isReloadNeeded() { + boolean reloadNeeded = false; + for (String path : keyPaths) { + long recentLastModifiedTimestamp = getLastModifiedTimestamp(path); + if (keyFilesLastModifiedTimestamps.get(path) != recentLastModifiedTimestamp) { + reloadNeeded = true; + keyFilesLastModifiedTimestamps.put(path, recentLastModifiedTimestamp); + } + } + for (String path : certPaths) { + long recentLastModifiedTimestamp = getLastModifiedTimestamp(path); + if (certFilesLastModifiedTimestamps.get(path) != recentLastModifiedTimestamp) { + reloadNeeded = true; + certFilesLastModifiedTimestamps.put(path, recentLastModifiedTimestamp); + } + } + return reloadNeeded; + } + @Override public PemKeyCertOptions copy() { return new PemKeyCertOptions(this); @@ -407,6 +515,11 @@ KeyStoreHelper getHelper(Vertx vertx) throws Exception { return helper; } + @Override + public void reload() { + this.helper = null; + } + /** * Load and return a Java keystore. * diff --git a/src/main/java/io/vertx/core/net/PfxOptions.java b/src/main/java/io/vertx/core/net/PfxOptions.java index 4c7090ff4dd..957a5c2d6b0 100644 --- a/src/main/java/io/vertx/core/net/PfxOptions.java +++ b/src/main/java/io/vertx/core/net/PfxOptions.java @@ -79,6 +79,16 @@ public PfxOptions setAliasPassword(String aliasPassword) { return (PfxOptions) super.setAliasPassword(aliasPassword); } + @Override + public PfxOptions setReloadCerts(Boolean reloadCerts) { + return (PfxOptions) super.setReloadCerts(reloadCerts); + } + + @Override + public PfxOptions setCertRefreshRateInSeconds(Long certRefreshRateInSeconds) { + return (PfxOptions) super.setCertRefreshRateInSeconds(certRefreshRateInSeconds); + } + @Override public PfxOptions copy() { return new PfxOptions(this); 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 23f4f2fa047..a2f59896b63 100755 --- a/src/main/java/io/vertx/core/net/impl/SSLHelper.java +++ b/src/main/java/io/vertx/core/net/impl/SSLHelper.java @@ -270,6 +270,16 @@ public SslContext createContext(VertxInternal vertx, String serverName, boolean } } + public void reloadContext() { + if (this.keyCertOptions.isReloadNeeded()) { + this.keyCertOptions.reload(); + this.sslContexts = new SslContext[2]; + this.sslContextMaps = new Map[] { + new ConcurrentHashMap<>(), new ConcurrentHashMap<>() + }; + } + } + public SslContext sslContext(VertxInternal vertx, String serverName, boolean useAlpn) { SslContext context = createContext(vertx, null, useAlpn, client, trustAll); return new DelegatingSslContext(context) { diff --git a/src/main/java/io/vertx/core/net/impl/TCPServerBase.java b/src/main/java/io/vertx/core/net/impl/TCPServerBase.java index f675fee48e4..773b6c722cb 100644 --- a/src/main/java/io/vertx/core/net/impl/TCPServerBase.java +++ b/src/main/java/io/vertx/core/net/impl/TCPServerBase.java @@ -29,6 +29,7 @@ import io.vertx.core.impl.VertxInternal; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.net.KeyCertOptions; import io.vertx.core.net.NetServerOptions; import io.vertx.core.net.SocketAddress; import io.vertx.core.spi.metrics.MetricsProvider; @@ -38,6 +39,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; /** * Base class for TCP servers @@ -82,7 +84,29 @@ public int actualPort() { protected abstract Handler childHandler(ContextInternal context, SocketAddress socketAddress, SSLHelper sslHelper); protected SSLHelper createSSLHelper() { - return new SSLHelper(options, null); + KeyCertOptions keyCertOptions = options.getKeyCertOptions(); + SSLHelper sslHelper = new SSLHelper(options, null); + startPeriodicSSLContextReload(keyCertOptions, sslHelper); + return sslHelper; + } + + protected void startPeriodicSSLContextReload(KeyCertOptions keyCertOptions, SSLHelper sslHelper) { + if (keyCertOptions == null) { + return; + } + + // start periodic context reload + if (keyCertOptions.getReloadCerts() != null && keyCertOptions.getReloadCerts()) { + vertx.setPeriodic(TimeUnit.SECONDS.toMillis(keyCertOptions.getCertRefreshRateInSeconds()), handle -> { + vertx.executeBlocking(future -> { + try { + sslHelper.reloadContext(); + } catch (Exception e) { + log.warn("SSL context reloading failed with exception {}", e); + } + }); + }); + } } public synchronized SSLHelper sslHelper() { diff --git a/src/test/java/io/vertx/core/net/SSLContextReloaderTest.java b/src/test/java/io/vertx/core/net/SSLContextReloaderTest.java new file mode 100644 index 00000000000..70b068e54c7 --- /dev/null +++ b/src/test/java/io/vertx/core/net/SSLContextReloaderTest.java @@ -0,0 +1,128 @@ +package io.vertx.core.net; + +/* + * Copyright (c) 2011-2019 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 + */ + +import io.vertx.core.Vertx; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.test.tls.Cert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class SSLContextReloaderTest { + private static final Logger log = LoggerFactory.getLogger(SSLContextReloaderTest.class); + + private Vertx vertx = Vertx.vertx(); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void testKeystoreReloaded() throws Exception { + Path certPath = tmp.newFile().toPath().toAbsolutePath(); + Files.copy(Paths.get("src/test/resources/tls/server-keystore.p12").toAbsolutePath(), certPath, StandardCopyOption.REPLACE_EXISTING); + HttpServerOptions serverOptions = new HttpServerOptions(); + serverOptions.setKeyStoreOptions(new JksOptions().setPath(certPath.toString()).setPassword("wibble").setReloadCerts(true).setCertRefreshRateInSeconds(1L)); + serverOptions.setTrustStoreOptions(new JksOptions().setPath("tls/server-truststore.p12").setPassword("wibble")); + serverOptions.setSsl(true); + + HttpServer server = vertx.createHttpServer(serverOptions).requestHandler(req -> { + req.response().end("success"); + }); + server.exceptionHandler(exp -> log.error("Exception thrown: " + exp)); + server.listen(9438, "localhost"); + + Promise> catchResult = Promise.promise(); + CountDownLatch latch = new CountDownLatch(1); + HttpClient client = getClient(getClientOptions("tls/client-truststore.p12")); + sendRequestToServerAndFailOnError(client, latch, catchResult); + latch.await(); + catchResult.future().onFailure(resp -> fail()); + + Files.copy(Paths.get("src/test/resources/tls/server-keystore-2.p12").toAbsolutePath(), certPath, StandardCopyOption.REPLACE_EXISTING); + + Promise> catchResultForNewTruststoreCall = Promise.promise(); + CountDownLatch latch2 = new CountDownLatch(1); + HttpClient clientWithNewTruststore = getClient(getClientOptions("tls/client-truststore-2.p12")); + while (!catchResultForNewTruststoreCall.future().isComplete()) { + sendRequestToServerWithoutFailing(clientWithNewTruststore, latch2, catchResultForNewTruststoreCall); // request should pass with new truststore + } + latch2.await(); + catchResultForNewTruststoreCall.future().onFailure(resp -> fail()); + + Promise> catchExpectedFailure = Promise.promise(); + CountDownLatch latch3 = new CountDownLatch(1); + HttpClient client3 = getClient(getClientOptions("tls/client-truststore.p12")); + sendRequestToServerAndFailOnError(client3, latch3, catchExpectedFailure); // request should fail with old client + latch3.await(); + catchExpectedFailure.future().onSuccess(resp -> fail()); + } + + private HttpClientOptions getClientOptions(String trustStore) { + HttpClientOptions clientOptions = new HttpClientOptions(); + clientOptions.setSsl(true); + clientOptions.setKeyStoreOptions(Cert.CLIENT_JKS.get()); + clientOptions.setTrustStoreOptions(new JksOptions().setPath(trustStore).setPassword("wibble")); + return clientOptions; + } + + private HttpClient getClient(HttpClientOptions clientOptions) { + return vertx.createHttpClient(clientOptions); + } + + private void sendRequestToServerAndFailOnError(HttpClient client, CountDownLatch latch, Promise> catchResult) { + client.request(HttpMethod.GET, 9438, "localhost", "/", req -> { + if (req.cause() != null) { + latch.countDown(); + catchResult.fail(req.cause()); + } else { + req.result().send(resp -> { + latch.countDown(); + catchResult.complete(resp.result().body()); + assertEquals(200, resp.result().statusCode()); + }); + } + }); + } + + private void sendRequestToServerWithoutFailing(HttpClient client, CountDownLatch latch, Promise> catchResult) { + client.request(HttpMethod.GET, 9438, "localhost", "/", req -> { + if (req.cause() == null) { + req.result().send(resp -> { + latch.countDown(); + if (!catchResult.future().isComplete()) { + catchResult.complete(resp.result().body()); + } + assertEquals(200, resp.result().statusCode()); + }); + } + }); + } +} diff --git a/src/test/resources/tls/client-truststore-2.p12 b/src/test/resources/tls/client-truststore-2.p12 new file mode 100644 index 0000000000000000000000000000000000000000..cf8a9e301a974b64d51419fb0a0f44b8f3302090 GIT binary patch literal 1186 zcmV;T1YP?uf&`ud0Ru3C1Xl(LDuzgg_YDCD0ic2eNCbieL@ zO0d_CbU|#Rt@<37UroQM2{oUFESn+@*-c=Qivqi&$r)Zut{DvH+D#)P`5=!2G#UNj zd?LkTZ3c<7=DFeL?MvC+>3Qj!KXvsOoK+?;E) z*DevDsK!>JO+Z2~&YoQZ{I}vFOS-1%4stxEXt~`%Ge9mQtBo_s7iqP?D!s-i>qW-4 zaH;xCee_2@x#%zU6+9sSGR#h>*KWt#tu%Q=uUNWi!T&iZDfDby4L$P_sqz{`giEDy zieZ`cSd;WiaHhGX3hywvVjD?h^^*auYr@WOh>|bqdbOUr;b%mDZk($Hudh4_trA)I z)ynw!vP`XEbHT8)$~g!M(vZ>pvxje=Eo8G`zB^z-^aRJ}h5B=hy9su@d&2A98LNIX zQ|)&{*nq%$WpC>Xyum>Fff9HPJ$`opFvo^%NQ;|Svr@ao;W_9cT%lO=fshbEOo)HF zWugGwB)_f}ComnPiRMe=f0I%{kU`@jEDPVPI(Jqaa%J*q>&(0UgaQT(rTB72Z}^^V z#(98l{>@+9QZ>|U^a*-T#$80$=E_C;<_jN2Qpyhh2ks)WuZrx@HG#Tb!cWpb=Q@qL z$7#!euQ!3jZ%+5h0hsbP@R+>)HMVcI@PCrn(5cJ;+I}3OT(V0Gf)`AlYg&WVOdg-Z z-F+rpEPQCWqWsdCe+Gw!>@ZSaR=`{E@ezY9_YER|Q{OIDv}*ZtJG2WEEPZiDy45738n`eT`HjDzYUU&ia7uUrko)rUx{a-x?> z_Z&v&trE?ss`2sZ80V}5LU-koAKm`+LPG%KwOV#x*P}Yl5G6DJJ?H!92aSidfq-B%9u^ISi6!VJ954Yd0E_Uj z-|6r$pYzxQji=-Jr-ByY>EP!PBM1mMe~^C~AQ}n1`0oWqAQa6^N3V=){x#EmqzVGj z18{gSsqVMF6Xp_TkECo-mFTXQq;TD%Lq$jVN<~w)$}a+BKRyljKoUGI6kweF)*`8Q z2`!2b9#IIiIX+ru7`C^n0?ue3sBO`=zD5aE3AZJ4k35Fh!0h0;d22Aci~@-5+qT?7 z+m_g?GUzZvS8=If1jUKlHe^06pSIZ@w^v+I5_ygA>kY!`tC#gHbSa`3&+h=625yu+ z0h#gbe2t%!=!6J8M<%}ztnGPHxCLgX+1Yh|^1Ez=am1vRvS01Er&@wHBTIQ)*|VkEXe1ycH^_@8S|3a` zn-|y<%xK#e7_&{dk1JAk^$ApKruo0Z7qb_Do|%g^CYk`kPc|+)C^C*}P{NaTZk-zU zipi|Z1``^1=_PciV5=f^v*+>6Uj!G62f+~dQ*kJ&*d}Pp%QOm`-)Q7O%aeuna2HlGF2K;>7;QK`k+}CcrSh>HXXV;vSiY0ZHnXyq z#a9x?&l4WWm=xEmdDlj*m3gvVf(^OKGv~q-rIkMCMk?W}MZVA_HwoJfhkS6Xoicr$ zu5j~iR;ncc=S8tu)ZCS{kLDbM$8ArV3R6c-dUG^uf6g}lXe2vz}DrDzW|bzC5|$4C=szY zJ7(-P`T}7Oe`izZe+!>a%wPjE3#Yy3?xk>20r?)ItbX1;of0Ruq0aXi4%U;wAETTT zrws|anqsp2TuFmoPv)mU)AGnj>RKYk6xU5YHD?hSTuIf}*y6k};uORC!*bSYor}** z*3k^0s0NVuSuWL@8`Z4NkZA-p-sf8=JDXabT}zw07MjqS1$VS(EMCsdSkrQ=5D4|n zUJ9t)D%9i<`T=rsYD$0G5h5bep_y;^jS0u%;a}t1a|mY5PCpZZ$J+a0IE1eTpz#p( z|AC+g50Rn6Lr~|j*!c-V8UDk2%63aLofv-!|Tlp;M%~C3|BGTvux0#~Hx_4~qzs|4(Pw$w3+ ziFs2WacP&uM44a6;F=a&?-476(h%EndJ4Rp{E4>$jKx1Gs1^|GzBCplalz^l!? zvP-Z&Pwk^&2n6$pjkkU|-htNjeZI#`7`orC*cFsg0{ZZ#0P7AL-19arjmn|;+C=Zx z!h`67&Y+#&BR-l!A!dUcWM7p@?Es-)i+pAIR5B{gAx0WtMBXZ)tC->r3&7q*qjK&sc@V=?AC*d*|xh1Nr(6 zQp!QAh%(XGaM}~&=U!LrvwnRDhYYIXRMg5@a{DbdWO#uH83w5NF zX0ig@o}!e>ag@YYYCoO(JH#c%#Glx?tHjD#$yb4jq58dPGl-gJ%L~hUvr12!>X3ik zI-UrMIDFPZ@4OP<6E@G7fycDJF5)d-0z9ujviIIzDv>wmh9Qkw$ZaJjbBPlnm-)Ns`+S_TAw#y5Yy_ z5o>FOwvA8ZU&Y=Tdzqddft!yfa1vb}A6zj98d?r*hmP}^SQ{UP@rQp`qpuoRPv&-| z+;dLYiSC)2@$=9eF?<1C_pDTJT&B9XpdVtdtfnlHiw1p55*FUgT#ad9);qFbpTnFw z2veb38>>vH*dKxBY2M&(fY)WpMrW%&6pAS>13L6+=tSu-*w(RWB~vZuVSK{yuB5u3 zPDa0Aw{i%gA5gTiUn_M}c^9r5!LQNuk$P+#I6Ydk5X5l9@T;qFiJvGtrK9qx zTe&C{9A*j+zBEOQUR4&8SAWZOA2KS${iS0x<(DKR`PSoci@0x_+ep;Jw1E0R@-k2#$OT~`x`dg>mK2p)5CWClC(FLvfs9>0HuZiG`8bsv2>Y+M@ZL&)`x zx%r$P4+V|g$+_I|VO6z(J-pQw9nTf~@x#2Z`K0H~8(C=~w7`FLgt1F=&>`-88$(SNm^R7iitY7a`P+j$ z2mR2}Xg)Ly3K3@pgSa37I>hw}d1o>)a1@4ZzcZ) D1KyYN literal 0 HcmV?d00001