diff --git a/java/google/registry/proxy/CertificateModule.java b/java/google/registry/proxy/CertificateModule.java index abfdbe42a..342a65e50 100644 --- a/java/google/registry/proxy/CertificateModule.java +++ b/java/google/registry/proxy/CertificateModule.java @@ -15,8 +15,10 @@ package google.registry.proxy; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Suppliers.memoizeWithExpiration; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.concurrent.TimeUnit.SECONDS; import com.google.common.collect.ImmutableList; import dagger.Lazy; @@ -32,7 +34,9 @@ import java.security.Security; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.function.Function; +import java.util.function.Supplier; import javax.inject.Named; +import javax.inject.Provider; import javax.inject.Qualifier; import javax.inject.Singleton; import org.bouncycastle.cert.X509CertificateHolder; @@ -62,11 +66,11 @@ public class CertificateModule { /** Dagger qualifier to provide bindings related to the certificates that the server provides. */ @Qualifier - @interface ServerCertificates {} + private @interface ServerCertificates {} /** Dagger qualifier to provide bindings when running locally. */ @Qualifier - @interface Local {} + private @interface Local {} /** * Dagger qualifier to provide bindings when running in production. @@ -99,6 +103,21 @@ public class CertificateModule { } @Singleton + @Provides + static Supplier providePrivateKeySupplier( + @ServerCertificates Provider privateKeyProvider, ProxyConfig config) { + return memoizeWithExpiration( + privateKeyProvider::get, config.serverCertificateCacheSeconds, SECONDS); + } + + @Singleton + @Provides + static Supplier provideCertificatesSupplier( + @ServerCertificates Provider certificatesProvider, ProxyConfig config) { + return memoizeWithExpiration( + certificatesProvider::get, config.serverCertificateCacheSeconds, SECONDS); + } + @Provides @ServerCertificates static X509Certificate[] provideCertificates( @@ -108,7 +127,6 @@ public class CertificateModule { return (env == Environment.LOCAL) ? localCertificates.get() : prodCertificates.get(); } - @Singleton @Provides @ServerCertificates static PrivateKey providePrivateKey( @@ -142,7 +160,6 @@ public class CertificateModule { return new X509Certificate[] {ssc.cert()}; } - @Singleton @Provides @Named("pemObjects") static ImmutableList providePemObjects(@Named("pemBytes") byte[] pemBytes) { @@ -167,7 +184,7 @@ public class CertificateModule { return listBuilder.build(); } - @Singleton + // This binding should not be used directly. Use the supplier binding instead. @Provides @Prod static PrivateKey provideProdPrivateKey(@Named("pemObjects") ImmutableList pemObjects) { @@ -190,7 +207,7 @@ public class CertificateModule { return privateKeys.get(0); } - @Singleton + // This binding should not be used directly. Use the supplier binding instead. @Provides @Prod static X509Certificate[] provideProdCertificates( diff --git a/java/google/registry/proxy/EppProtocolModule.java b/java/google/registry/proxy/EppProtocolModule.java index e5d3f8d06..37228819d 100644 --- a/java/google/registry/proxy/EppProtocolModule.java +++ b/java/google/registry/proxy/EppProtocolModule.java @@ -20,7 +20,6 @@ import com.google.common.collect.ImmutableList; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoSet; -import google.registry.proxy.CertificateModule.ServerCertificates; import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol; import google.registry.proxy.Protocol.BackendProtocol; import google.registry.proxy.Protocol.FrontendProtocol; @@ -161,9 +160,9 @@ public class EppProtocolModule { @EppProtocol static SslServerInitializer provideSslServerInitializer( SslProvider sslProvider, - @ServerCertificates PrivateKey privateKey, - @ServerCertificates X509Certificate... certificates) { - return new SslServerInitializer<>(true, sslProvider, privateKey, certificates); + Supplier privateKeySupplier, + Supplier certificatesSupplier) { + return new SslServerInitializer<>(true, sslProvider, privateKeySupplier, certificatesSupplier); } @Provides diff --git a/java/google/registry/proxy/ProxyConfig.java b/java/google/registry/proxy/ProxyConfig.java index 16a14bab2..d47e1452d 100644 --- a/java/google/registry/proxy/ProxyConfig.java +++ b/java/google/registry/proxy/ProxyConfig.java @@ -39,6 +39,7 @@ public class ProxyConfig { public List gcpScopes; public int accessTokenValidPeriodSeconds; public int accessTokenRefreshBeforeExpirySeconds; + public int serverCertificateCacheSeconds; public Gcs gcs; public Kms kms; public Epp epp; diff --git a/java/google/registry/proxy/ProxyModule.java b/java/google/registry/proxy/ProxyModule.java index 1c3bf9109..9c6efbb29 100644 --- a/java/google/registry/proxy/ProxyModule.java +++ b/java/google/registry/proxy/ProxyModule.java @@ -260,7 +260,7 @@ public class ProxyModule { .build(); } - @Singleton + // This binding should not be used directly. Use those provided in CertificateModule instead. @Provides @Named("encryptedPemBytes") static byte[] provideEncryptedPemBytes(Storage storage, ProxyConfig config) { @@ -280,7 +280,7 @@ public class ProxyModule { } } - @Singleton + // This binding should not be used directly. Use those provided in CertificateModule instead. @Provides @Named("pemBytes") static byte[] providePemBytes( diff --git a/java/google/registry/proxy/WebWhoisProtocolsModule.java b/java/google/registry/proxy/WebWhoisProtocolsModule.java index eb612b53b..d1f6d0341 100644 --- a/java/google/registry/proxy/WebWhoisProtocolsModule.java +++ b/java/google/registry/proxy/WebWhoisProtocolsModule.java @@ -18,7 +18,6 @@ import com.google.common.collect.ImmutableList; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoSet; -import google.registry.proxy.CertificateModule.ServerCertificates; import google.registry.proxy.Protocol.FrontendProtocol; import google.registry.proxy.handler.SslServerInitializer; import google.registry.proxy.handler.WebWhoisRedirectHandler; @@ -29,6 +28,7 @@ import io.netty.handler.codec.http.HttpServerExpectContinueHandler; import io.netty.handler.ssl.SslProvider; import java.security.PrivateKey; import java.security.cert.X509Certificate; +import java.util.function.Supplier; import javax.inject.Provider; import javax.inject.Qualifier; import javax.inject.Singleton; @@ -132,8 +132,8 @@ public class WebWhoisProtocolsModule { @HttpsWhoisProtocol static SslServerInitializer provideSslServerInitializer( SslProvider sslProvider, - @ServerCertificates PrivateKey privateKey, - @ServerCertificates X509Certificate... certificates) { - return new SslServerInitializer<>(false, sslProvider, privateKey, certificates); + Supplier privateKeySupplier, + Supplier certificatesSupplier) { + return new SslServerInitializer<>(false, sslProvider, privateKeySupplier, certificatesSupplier); } } diff --git a/java/google/registry/proxy/config/default-config.yaml b/java/google/registry/proxy/config/default-config.yaml index 5bf54e835..32fb6f9c1 100644 --- a/java/google/registry/proxy/config/default-config.yaml +++ b/java/google/registry/proxy/config/default-config.yaml @@ -36,6 +36,14 @@ accessTokenValidPeriodSeconds: 1800 # com.google.api.client.auth.oauth2.Credential#intercept. accessTokenRefreshBeforeExpirySeconds: 60 +# Server certificate is cached for 30 minutes. +# +# Encrypted server server certificate and private keys are stored on GCS. They +# are cached and shared for all connections for 30 minutes. We not not cache +# the certificate indefinitely because if we upload a new one to GCS, all +# existing instances need to be killed if they cache the old one indefinitely. +serverCertificateCacheSeconds: 1800 + gcs: # GCS bucket that stores the encrypted PEM file. bucket: your-gcs-bucket diff --git a/java/google/registry/proxy/handler/SslServerInitializer.java b/java/google/registry/proxy/handler/SslServerInitializer.java index 0005155c7..c58871c87 100644 --- a/java/google/registry/proxy/handler/SslServerInitializer.java +++ b/java/google/registry/proxy/handler/SslServerInitializer.java @@ -29,6 +29,7 @@ import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; import java.security.PrivateKey; import java.security.cert.X509Certificate; +import java.util.function.Supplier; /** * Adds a server side SSL handler to the channel pipeline. @@ -57,25 +58,25 @@ public class SslServerInitializer extends ChannelInitializer< private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final boolean requireClientCert; private final SslProvider sslProvider; - private final PrivateKey privateKey; - private final X509Certificate[] certificates; + private final Supplier privateKeySupplier; + private final Supplier certificatesSupplier; public SslServerInitializer( boolean requireClientCert, SslProvider sslProvider, - PrivateKey privateKey, - X509Certificate... certificates) { + Supplier privateKeySupplier, + Supplier certificatesSupplier) { logger.atInfo().log("Server SSL Provider: %s", sslProvider); this.requireClientCert = requireClientCert; this.sslProvider = sslProvider; - this.privateKey = privateKey; - this.certificates = certificates; + this.privateKeySupplier = privateKeySupplier; + this.certificatesSupplier = certificatesSupplier; } @Override protected void initChannel(C channel) throws Exception { SslHandler sslHandler = - SslContextBuilder.forServer(privateKey, certificates) + SslContextBuilder.forServer(privateKeySupplier.get(), certificatesSupplier.get()) .sslProvider(sslProvider) .trustManager(InsecureTrustManagerFactory.INSTANCE) .clientAuth(requireClientCert ? ClientAuth.REQUIRE : ClientAuth.NONE) diff --git a/javatests/google/registry/proxy/handler/SslServerInitializerTest.java b/javatests/google/registry/proxy/handler/SslServerInitializerTest.java index d18ca44aa..3b88816a0 100644 --- a/javatests/google/registry/proxy/handler/SslServerInitializerTest.java +++ b/javatests/google/registry/proxy/handler/SslServerInitializerTest.java @@ -21,6 +21,7 @@ import static google.registry.proxy.handler.SslInitializerTestUtils.setUpServer; import static google.registry.proxy.handler.SslInitializerTestUtils.signKeyPair; import static google.registry.proxy.handler.SslInitializerTestUtils.verifySslChannel; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import google.registry.proxy.Protocol; import google.registry.proxy.Protocol.BackendProtocol; @@ -99,7 +100,10 @@ public class SslServerInitializerTest { ch.pipeline() .addLast( new SslServerInitializer( - requireClientCert, SslProvider.JDK, privateKey, certificates), + requireClientCert, + SslProvider.JDK, + Suppliers.ofInstance(privateKey), + Suppliers.ofInstance(certificates)), new EchoHandler(serverLock, serverException)); } }; @@ -148,7 +152,11 @@ public class SslServerInitializerTest { public void testSuccess_swappedInitializerWithSslHandler() throws Exception { SelfSignedCertificate ssc = new SelfSignedCertificate(SSL_HOST); SslServerInitializer sslServerInitializer = - new SslServerInitializer<>(true, SslProvider.JDK, ssc.key(), ssc.cert()); + new SslServerInitializer<>( + true, + SslProvider.JDK, + Suppliers.ofInstance(ssc.key()), + Suppliers.ofInstance(new X509Certificate[] {ssc.cert()})); EmbeddedChannel channel = new EmbeddedChannel(); ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast(sslServerInitializer);