Consolidate certificate supplier module (#410)

* Consolidate certificate supplier module

Both the proxy and the proxy needs certificate suppliers. The PR
consolidates the module that providings those bindings to a shared
module and switched the proxy to use that module. The prober currently
uses P12 file to store its certificates. I am debating keeping that
supplier ro converting them to PEM files for simplicity.

* Rename mode enum values to be more descriptive

* Update annotation names to be more descriptive
This commit is contained in:
Lai Jiang 2019-12-23 13:09:47 -05:00 committed by GitHub
parent 24d671c070
commit dd0e3b7c24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 909 additions and 758 deletions

View file

@ -0,0 +1,301 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.networking.module;
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.base.Suppliers;
import com.google.common.collect.ImmutableList;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Duration;
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;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMException;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
/**
* Dagger module that provides bindings needed to inject a certificate chain the corresponding
* private key.
*
* <p>There are several ways ({@link Mode}s) that the certificate/key can be provided (explained
* later), and the user of this module needs to provide the binding to the {@link Mode} in some
* other module to the component.
*
* <p>The production certificates and private key are stored in a .pem file that is encrypted by
* Cloud KMS. The .pem file can be generated by concatenating the .crt certificate files on the
* chain and the .key private file.
*
* <p>The production certificates in the .pem file must be stored in order, where the next
* certificate's subject is the previous certificate's issuer.
*
* <p>When running the proxy locally or in test, a self signed certificate is used.
*
* @see <a href="https://cloud.google.com/kms/">Cloud Key Management Service</a>
*/
@Module
public final class CertificateSupplierModule {
static {
Security.addProvider(new BouncyCastleProvider());
}
private CertificateSupplierModule() {}
public enum Mode {
/**
* The certificate chain and the private key are stored in a single PEM file.
*
* <p>Both certificates and private key are Base64 encoded and the single PEM file is generated
* by concatenating each ASCII-armored block. The private key can appear anywhere in the list
* but the certificates must appear in order, i. e. the next certificate's subject is the
* previous certificates's issuer.
*
* @see <a href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">Privacy-Enhanced Email</a>
*/
PEM_FILE,
P12_FILE,
/**
* A single certificate/private key pair is generated in place and self signed. Used in tests.
*/
SELF_SIGNED
}
@Qualifier
@interface PemFile {}
@Qualifier
private @interface P12File {}
@Qualifier
private @interface SelfSigned {}
/**
* Select specific type from a given {@link ImmutableList} and convert them using the converter.
*
* @param objects the {@link ImmutableList} to filter from.
* @param clazz the class to filter.
* @param converter the converter function to act on the items in the filtered list.
*/
private static <T, E> ImmutableList<E> filterAndConvert(
ImmutableList<Object> objects, Class<T> clazz, Function<T, E> converter) {
return objects.stream()
.filter(clazz::isInstance)
.map(clazz::cast)
.map(converter)
.collect(toImmutableList());
}
@Singleton
@Provides
static Supplier<PrivateKey> providePrivateKeySupplier(
Mode mode,
@PemFile Lazy<Supplier<PrivateKey>> pemPrivateKeySupplier,
@P12File Lazy<Supplier<PrivateKey>> p12PrivateKeySupplier,
@SelfSigned Lazy<Supplier<PrivateKey>> selfSignedPrivateKeySupplier) {
switch (mode) {
case PEM_FILE:
return pemPrivateKeySupplier.get();
case P12_FILE:
return p12PrivateKeySupplier.get();
case SELF_SIGNED:
return selfSignedPrivateKeySupplier.get();
default:
throw new RuntimeException("Certificate provider mode exhausted.");
}
}
@Singleton
@Provides
static Supplier<ImmutableList<X509Certificate>> provideCertificatesSupplier(
Mode mode,
@PemFile Lazy<Supplier<ImmutableList<X509Certificate>>> pemCertificatesSupplier,
@P12File Lazy<Supplier<ImmutableList<X509Certificate>>> p12CertificatesSupplier,
@SelfSigned Lazy<Supplier<ImmutableList<X509Certificate>>> selfSignedCertificatesSupplier) {
switch (mode) {
case PEM_FILE:
return pemCertificatesSupplier.get();
case P12_FILE:
return p12CertificatesSupplier.get();
case SELF_SIGNED:
return selfSignedCertificatesSupplier.get();
default:
throw new RuntimeException("Certificate provider mode exhausted.");
}
}
@Singleton
@Provides
static SelfSignedCertificate provideSelfSignedCertificate() {
try {
return new SelfSignedCertificate();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Singleton
@Provides
@SelfSigned
static Supplier<PrivateKey> provideSelfSignedPrivateKeySupplier(SelfSignedCertificate ssc) {
return Suppliers.ofInstance(ssc.key());
}
@Singleton
@Provides
@SelfSigned
static Supplier<ImmutableList<X509Certificate>> provideSelfSignedCertificatesSupplier(
SelfSignedCertificate ssc) {
return Suppliers.ofInstance(ImmutableList.of(ssc.cert()));
}
@Provides
@PemFile
static ImmutableList<Object> providePemObjects(@Named("pemBytes") byte[] pemBytes) {
PEMParser pemParser =
new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes), UTF_8));
ImmutableList.Builder<Object> listBuilder = new ImmutableList.Builder<>();
Object obj;
// PEMParser returns an object (private key, certificate, etc) each time readObject() is called,
// until no more object is to be read from the file.
while (true) {
try {
obj = pemParser.readObject();
if (obj == null) {
break;
} else {
listBuilder.add(obj);
}
} catch (IOException e) {
throw new RuntimeException("Cannot parse PEM file correctly.", e);
}
}
return listBuilder.build();
}
// This binding should not be used directly. Use the supplier binding instead.
@Provides
@PemFile
static PrivateKey providePemPrivateKey(@PemFile ImmutableList<Object> pemObjects) {
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
Function<PEMKeyPair, PrivateKey> privateKeyConverter =
pemKeyPair -> {
try {
return converter.getKeyPair(pemKeyPair).getPrivate();
} catch (PEMException e) {
throw new RuntimeException(
String.format("Error converting private key: %s", pemKeyPair), e);
}
};
ImmutableList<PrivateKey> privateKeys =
filterAndConvert(pemObjects, PEMKeyPair.class, privateKeyConverter);
checkState(
privateKeys.size() == 1,
"The pem file must contain exactly one private key, but %s keys are found",
privateKeys.size());
return privateKeys.get(0);
}
// This binding should not be used directly. Use the supplier binding instead.
@Provides
@PemFile
static ImmutableList<X509Certificate> providePemCertificates(
@PemFile ImmutableList<Object> pemObject) {
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider("BC");
Function<X509CertificateHolder, X509Certificate> certificateConverter =
certificateHolder -> {
try {
return converter.getCertificate(certificateHolder);
} catch (CertificateException e) {
throw new RuntimeException(
String.format("Error converting certificate: %s", certificateHolder), e);
}
};
ImmutableList<X509Certificate> certificates =
filterAndConvert(pemObject, X509CertificateHolder.class, certificateConverter);
checkState(!certificates.isEmpty(), "No certificates found in the pem file");
X509Certificate lastCert = null;
for (X509Certificate cert : certificates) {
if (lastCert != null) {
checkState(
lastCert.getIssuerX500Principal().equals(cert.getSubjectX500Principal()),
"Certificate chain error:\n%s\nis not signed by\n%s",
lastCert,
cert);
}
lastCert = cert;
}
return certificates;
}
@Singleton
@Provides
@PemFile
static Supplier<PrivateKey> providePemPrivateKeySupplier(
@PemFile Provider<PrivateKey> privateKeyProvider,
@Named("remoteCertCachingDuration") Duration cachingDuration) {
return memoizeWithExpiration(privateKeyProvider::get, cachingDuration.getSeconds(), SECONDS);
}
@Singleton
@Provides
@PemFile
static Supplier<ImmutableList<X509Certificate>> providePemCertificatesSupplier(
@PemFile Provider<ImmutableList<X509Certificate>> certificatesProvider,
@Named("remoteCertCachingDuration") Duration cachingDuration) {
return memoizeWithExpiration(certificatesProvider::get, cachingDuration.getSeconds(), SECONDS);
}
// TODO(jianglai): Implement P12 supplier or convert the file to PEM format.
@Singleton
@Provides
@P12File
static Supplier<PrivateKey> provideP12PrivateKeySupplier() {
return Suppliers.ofInstance(null);
}
// TODO(jianglai): Implement P12 supplier or convert the file to PEM format.
@Singleton
@Provides
@P12File
static Supplier<ImmutableList<X509Certificate>> provideP12CertificatesSupplier() {
return Suppliers.ofInstance(null);
}
}

View file

@ -0,0 +1,181 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.networking.module;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.networking.handler.SslInitializerTestUtils.getKeyPair;
import static google.registry.networking.handler.SslInitializerTestUtils.signKeyPair;
import static google.registry.testing.JUnitBackports.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import dagger.BindsInstance;
import dagger.Component;
import dagger.Module;
import dagger.Provides;
import google.registry.networking.module.CertificateSupplierModule.Mode;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.function.Supplier;
import javax.inject.Named;
import javax.inject.Singleton;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link CertificateSupplierModule}. */
@RunWith(JUnit4.class)
public class CertificateSupplierModuleTest {
private SelfSignedCertificate ssc;
private PrivateKey key;
private Certificate cert;
private TestComponent component;
/** Create a component with bindings to construct certificates and keys from a PEM file. */
private static TestComponent createComponentForPem(Object... objects) throws Exception {
return DaggerCertificateSupplierModuleTest_TestComponent.builder()
.certificateModule(CertificateModule.createCertificateModuleForPem(objects))
.useMode(Mode.PEM_FILE)
.build();
}
@Before
public void setUp() throws Exception {
ssc = new SelfSignedCertificate();
KeyPair keyPair = getKeyPair();
key = keyPair.getPrivate();
cert = signKeyPair(ssc, keyPair, "example.tld");
}
@Test
public void testSuccess() throws Exception {
component = createComponentForPem(cert, ssc.cert(), key);
assertThat(component.privateKeySupplier().get()).isEqualTo(key);
assertThat(component.certificatesSupplier().get()).containsExactly(cert, ssc.cert()).inOrder();
}
@Test
public void testSuccess_certificateChainNotContinuous() throws Exception {
component = createComponentForPem(cert, key, ssc.cert());
assertThat(component.privateKeySupplier().get()).isEqualTo(key);
assertThat(component.certificatesSupplier().get()).containsExactly(cert, ssc.cert()).inOrder();
}
@Test
@SuppressWarnings("ReturnValueIgnored")
public void testFailure_noPrivateKey() throws Exception {
component = createComponentForPem(cert, ssc.cert());
IllegalStateException thrown =
assertThrows(IllegalStateException.class, () -> component.privateKeySupplier().get());
assertThat(thrown).hasMessageThat().contains("0 keys are found");
}
@Test
@SuppressWarnings("ReturnValueIgnored")
public void testFailure_twoPrivateKeys() throws Exception {
component = createComponentForPem(cert, ssc.cert(), key, ssc.key());
IllegalStateException thrown =
assertThrows(IllegalStateException.class, () -> component.privateKeySupplier().get());
assertThat(thrown).hasMessageThat().contains("2 keys are found");
}
@Test
@SuppressWarnings("ReturnValueIgnored")
public void testFailure_certificatesOutOfOrder() throws Exception {
component = createComponentForPem(ssc.cert(), cert, key);
IllegalStateException thrown =
assertThrows(IllegalStateException.class, () -> component.certificatesSupplier().get());
assertThat(thrown).hasMessageThat().contains("is not signed by");
}
@Test
@SuppressWarnings("ReturnValueIgnored")
public void testFailure_noCertificates() throws Exception {
component = createComponentForPem(key);
IllegalStateException thrown =
assertThrows(IllegalStateException.class, () -> component.certificatesSupplier().get());
assertThat(thrown).hasMessageThat().contains("No certificates");
}
@Module
static class CertificateModule {
private final byte[] pemBytes;
private CertificateModule(byte[] pemBytes) {
this.pemBytes = pemBytes.clone();
}
static CertificateModule createCertificateModuleForPem(Object... objects) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (JcaPEMWriter pemWriter =
new JcaPEMWriter(new OutputStreamWriter(byteArrayOutputStream, UTF_8))) {
for (Object object : objects) {
pemWriter.writeObject(object);
}
}
return new CertificateModule(byteArrayOutputStream.toByteArray());
}
@Provides
@Named("pemBytes")
byte[] providePemBytes() {
return pemBytes.clone();
}
@Provides
@Named("remoteCertCachingDuration")
Duration provideCachingDuration() {
// Make the supplier always return the save value for test to save time.
return Duration.ofDays(1);
}
}
/**
* Test component that exposes the certificates and private key.
*
* <p>Depending on what {@link Mode} is provided to the component, it will return certificates and
* private key extracted from the correspondonig format. Self-signed mode is not tested as they
* are only used in tests.
*/
@Singleton
@Component(modules = {CertificateSupplierModule.class, CertificateModule.class})
interface TestComponent {
Supplier<PrivateKey> privateKeySupplier();
Supplier<ImmutableList<X509Certificate>> certificatesSupplier();
@Component.Builder
interface Builder {
@BindsInstance
Builder useMode(Mode mode);
Builder certificateModule(CertificateModule certificateModule);
TestComponent build();
}
}
}