diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java index dae13c3cb..e26fc72d9 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java @@ -24,6 +24,10 @@ public class SecureStorageServiceConfiguration { @JsonProperty private String uri; + @NotBlank + @JsonProperty + private String storageCaCertificate; + @NotNull @Valid @JsonProperty @@ -47,6 +51,15 @@ public class SecureStorageServiceConfiguration { return uri; } + @VisibleForTesting + public void setStorageCaCertificate(final String certificatePem) { + this.storageCaCertificate = certificatePem; + } + + public String getStorageCaCertificate() { + return storageCaCertificate; + } + public CircuitBreakerConfiguration getCircuitBreakerConfiguration() { return circuitBreaker; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java index f3c383731..0513cfcf6 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java @@ -17,6 +17,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; import java.time.Duration; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -34,7 +35,7 @@ public class SecureStorageClient { @VisibleForTesting static final String DELETE_PATH = "/v1/storage"; - public SecureStorageClient(final ExternalServiceCredentialGenerator storageServiceCredentialGenerator, final Executor executor, final SecureStorageServiceConfiguration configuration) { + public SecureStorageClient(final ExternalServiceCredentialGenerator storageServiceCredentialGenerator, final Executor executor, final SecureStorageServiceConfiguration configuration) throws CertificateException { this.storageServiceCredentialGenerator = storageServiceCredentialGenerator; this.deleteUri = URI.create(configuration.getUri()).resolve(DELETE_PATH); this.httpClient = FaultTolerantHttpClient.newBuilder() @@ -45,6 +46,7 @@ public class SecureStorageClient { .withRedirect(HttpClient.Redirect.NEVER) .withExecutor(executor) .withName("secure-storage") + .withTrustedServerCertificate(configuration.getStorageCaCertificate()) .build(); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/DirectoryReconciliationClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/DirectoryReconciliationClient.java index 6c5c91df7..37d4484a1 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/DirectoryReconciliationClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/DirectoryReconciliationClient.java @@ -5,13 +5,13 @@ package org.whispersystems.textsecuregcm.storage; import com.codahale.metrics.SharedMetricRegistries; -import org.bouncycastle.openssl.PEMReader; import org.glassfish.jersey.SslConfigurator; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; import org.whispersystems.textsecuregcm.configuration.DirectoryServerConfiguration; import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationRequest; import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationResponse; import org.whispersystems.textsecuregcm.util.CertificateExpirationGauge; +import org.whispersystems.textsecuregcm.util.CertificateUtil; import org.whispersystems.textsecuregcm.util.Constants; import javax.net.ssl.SSLContext; @@ -19,14 +19,8 @@ import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStreamReader; import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; import static com.codahale.metrics.MetricRegistry.name; @@ -43,7 +37,7 @@ public class DirectoryReconciliationClient { SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME) .register(name(getClass(), directoryServerConfiguration.getReplicationName(), "days_until_certificate_expiration"), - new CertificateExpirationGauge(getCertificate(directoryServerConfiguration.getReplicationCaCertificate()))); + new CertificateExpirationGauge(CertificateUtil.getCertificate(directoryServerConfiguration.getReplicationCaCertificate()))); } public DirectoryReconciliationResponse sendChunk(DirectoryReconciliationRequest request) { @@ -56,7 +50,7 @@ public class DirectoryReconciliationClient { private static Client initializeClient(DirectoryServerConfiguration directoryServerConfiguration) throws CertificateException { - KeyStore trustStore = initializeKeyStore(directoryServerConfiguration.getReplicationCaCertificate()); + KeyStore trustStore = CertificateUtil.buildKeyStoreForPem(directoryServerConfiguration.getReplicationCaCertificate()); SSLContext sslContext = SslConfigurator.newInstance() .securityProtocol("TLSv1.2") .trustStore(trustStore) @@ -66,33 +60,4 @@ public class DirectoryReconciliationClient { .sslContext(sslContext) .build(); } - - private static KeyStore initializeKeyStore(String caCertificatePem) - throws CertificateException - { - try { - X509Certificate certificate = getCertificate(caCertificatePem); - - if (certificate == null) { - throw new CertificateException("No certificate found in parsing!"); - } - - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(null); - keyStore.setCertificateEntry("ca", certificate); - return keyStore; - } catch (IOException | KeyStoreException ex) { - throw new CertificateException(ex); - } catch (NoSuchAlgorithmException ex) { - throw new AssertionError(ex); - } - } - - private static X509Certificate getCertificate(final String certificatePem) throws CertificateException { - try (PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(certificatePem.getBytes())))) { - return (X509Certificate) reader.readObject(); - } catch (IOException e) { - throw new CertificateException(e); - } - } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/CertificateUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/CertificateUtil.java new file mode 100644 index 000000000..9eed5d4c4 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/CertificateUtil.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import org.bouncycastle.openssl.PEMReader; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class CertificateUtil { + public static KeyStore buildKeyStoreForPem(final String caCertificatePem) throws CertificateException + { + try { + X509Certificate certificate = getCertificate(caCertificatePem); + + if (certificate == null) { + throw new CertificateException("No certificate found in parsing!"); + } + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + keyStore.setCertificateEntry("ca", certificate); + return keyStore; + } catch (IOException | KeyStoreException ex) { + throw new CertificateException(ex); + } catch (NoSuchAlgorithmException ex) { + throw new AssertionError(ex); + } + } + + public static X509Certificate getCertificate(final String certificatePem) throws CertificateException { + try (PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(certificatePem.getBytes())))) { + return (X509Certificate) reader.readObject(); + } catch (IOException e) { + throw new CertificateException(e); + } + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClientTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClientTest.java index e4ceb7001..39306c27a 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClientTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClientTest.java @@ -7,14 +7,19 @@ package org.whispersystems.textsecuregcm.securestorage; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.apache.commons.lang3.RandomStringUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration; +import java.security.Security; +import java.security.cert.CertificateException; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -39,8 +44,13 @@ public class SecureStorageClientTest { @Rule public WireMockRule wireMockRule = new WireMockRule(options().dynamicPort().dynamicHttpsPort()); + @BeforeClass + public static void setupBeforeClass() { + Security.addProvider(new BouncyCastleProvider()); + } + @Before - public void setUp() { + public void setUp() throws CertificateException { accountUuid = UUID.randomUUID(); credentialGenerator = mock(ExternalServiceCredentialGenerator.class); httpExecutor = Executors.newSingleThreadExecutor(); @@ -48,6 +58,24 @@ public class SecureStorageClientTest { final SecureStorageServiceConfiguration config = new SecureStorageServiceConfiguration(); config.setUri("http://localhost:" + wireMockRule.port()); + // This is a randomly-generated, throwaway certificate that's not actually connected to anything + config.setStorageCaCertificate( + "-----BEGIN CERTIFICATE-----\n" + + "MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL\n" + + "MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG\n" + + "A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla\n" + + "ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l\n" + + "c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB\n" + + "AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y\n" + + "/X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD\n" + + "ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID\n" + + "AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw\n" + + "FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B\n" + + "AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa\n" + + "y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr\n" + + "R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ==\n" + + "-----END CERTIFICATE-----"); + secureStorageClient = new SecureStorageClient(credentialGenerator, httpExecutor, config); } @@ -57,6 +85,11 @@ public class SecureStorageClientTest { httpExecutor.awaitTermination(1, TimeUnit.SECONDS); } + @AfterClass + public static void tearDownAfterClass() { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } + @Test public void deleteStoredData() { final String username = RandomStringUtils.randomAlphabetic(16);