diff --git a/pom.xml b/pom.xml
index 09fc072cc..c283b341d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -78,6 +78,8 @@
4.1.0
1.17.1
4.2.18.Final
+
+ 2.0.84.Final
4.36.0
@@ -141,6 +143,11 @@
pom
import
+
+ io.netty
+ netty-tcnative-boringssl-static
+ ${netty-tcnative.version}
+
com.google.protobuf
protobuf-bom
diff --git a/service/pom.xml b/service/pom.xml
index a8361ed58..089e7e0f0 100644
--- a/service/pom.xml
+++ b/service/pom.xml
@@ -513,6 +513,11 @@
netty-resolver-dns
+
+ io.netty
+ netty-tcnative-boringssl-static
+
+
io.netty
netty-transport
@@ -795,6 +800,7 @@
-Djdk.nio.maxCachedBufferSize=262144
-Dlog4j2.formatMsgNoLookups=true
-Djdk.tls.server.newSessionTicketCount=0
+ -Djdk.tls.server.enableSessionTicketExtension=false
-XX:MaxRAMPercentage=75
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.bin
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
index a644623da..b2615ef00 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
@@ -666,7 +666,7 @@ public class WhisperServerService extends Application dnsResolutionEventLoopGroup = new ManagedEventLoopGroup<>(new NioEventLoopGroup());
final DnsNameResolver cloudflareDnsResolver = new DnsNameResolverBuilder(dnsResolutionEventLoopGroup.getEventLoopGroup().next())
- .resolvedAddressTypes(ResolvedAddressTypes.IPV6_PREFERRED)
- .completeOncePreferredResolved(false)
- .channelType(NioDatagramChannel.class)
- .socketChannelType(NioSocketChannel.class)
- .build();
+ .resolvedAddressTypes(ResolvedAddressTypes.IPV6_PREFERRED)
+ .completeOncePreferredResolved(false)
+ .channelType(NioDatagramChannel.class)
+ .socketChannelType(NioSocketChannel.class)
+ .build();
ExternalServiceCredentialsGenerator directoryV2CredentialsGenerator = DirectoryV2Controller.credentialsGenerator(
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration());
@@ -1218,7 +1218,9 @@ public class WhisperServerService extends Application sniMapping = config.getGrpc().h2c()
? null
- : SniMapper.buildSniMapping(config.getTlsKeyStoreConfiguration().path(), config.getTlsKeyStoreConfiguration().password().value());
+ : SniMapper.buildSniMapping(config.getTlsKeyStoreConfiguration().path(),
+ config.getTlsKeyStoreConfiguration().password().value(),
+ config.getGrpc().sslProvider());
final OmnibusH2Server omnibusH2Server = new OmnibusH2Server(
sniMapping,
omnibusNioEventLoopGroup.getEventLoopGroup(),
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GrpcConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GrpcConfiguration.java
index 295879b8f..5c72f25ba 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GrpcConfiguration.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GrpcConfiguration.java
@@ -4,6 +4,7 @@
*/
package org.whispersystems.textsecuregcm.configuration;
+import io.netty.handler.ssl.SslProvider;
import jakarta.validation.constraints.NotNull;
import java.time.Duration;
@@ -15,13 +16,15 @@ import java.time.Duration;
/// @param websocketPort The port of a listening websocket server for handling legacy requests
/// @param idleTimeout The duration after which an idle connection may be disconnected
/// @param h2c If true, listen for plaintext h2c with prior-knowledge
+/// @param sslProvider The TLS implementation used to terminate TLS. Defaults to `OPENSSL`.
public record GrpcConfiguration(
@NotNull String bindAddress,
@NotNull Integer port,
@NotNull String websocketAddress,
@NotNull Integer websocketPort,
@NotNull Duration idleTimeout,
- boolean h2c) {
+ boolean h2c,
+ @NotNull SslProvider sslProvider) {
public GrpcConfiguration {
if (bindAddress == null || bindAddress.isEmpty()) {
@@ -33,5 +36,8 @@ public record GrpcConfiguration(
if (idleTimeout == null) {
idleTimeout = Duration.ofMinutes(5);
}
+ if (sslProvider == null) {
+ sslProvider = SslProvider.OPENSSL;
+ }
}
}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/BoringSslServerKeyManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/BoringSslServerKeyManager.java
new file mode 100644
index 000000000..b2cd4f656
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/BoringSslServerKeyManager.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2026 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+package org.whispersystems.textsecuregcm.grpc.net;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.netty.handler.ssl.OpenSsl;
+import io.netty.handler.ssl.ReferenceCountedOpenSslEngine;
+import io.netty.internal.tcnative.SSL;
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509KeyManager;
+
+/// A server key manager for netty's BoringSSL provider that uses a client's signature algorithm preferences, enabling
+/// support for ed25519.
+///
+/// This works around [netty#10916](https://github.com/netty/netty/issues/10916) by addressing two issues in key selection:
+///
+/// 1. Netty’s engine only declares support for RSA- and EC-family key types, and
+/// 2. the handshake session's peer signature algorithm converter (`io.netty.handler.ssl.SignatureAlgorithmConverter`)
+/// doesn't match BoringSSL's `ed25519`, so ed25519 gets dropped, even if one is presented as a candidate
+class BoringSslServerKeyManager extends X509ExtendedKeyManager {
+
+ private final X509KeyManager delegate;
+
+ @VisibleForTesting
+ BoringSslServerKeyManager(final X509KeyManager delegate) {
+ if (!"BoringSSL".equals(OpenSsl.versionString())) {
+ throw new IllegalStateException("BoringSSL required, found " + OpenSsl.versionString());
+ }
+ this.delegate = delegate;
+ }
+
+ /// @throws IllegalArgumentException if `keyManagers` contains no [X509KeyManager]
+ static BoringSslServerKeyManager wrap(final KeyManager[] keyManagers) {
+ return Arrays.stream(keyManagers)
+ .filter(X509KeyManager.class::isInstance)
+ .map(X509KeyManager.class::cast)
+ .findFirst()
+ .map(BoringSslServerKeyManager::new)
+ .orElseThrow(() -> new IllegalArgumentException("No X509KeyManager found"));
+ }
+
+ @Override
+ public String chooseEngineServerAlias(final String keyType, final Principal[] issuers, final SSLEngine engine) {
+ if (!(engine instanceof ReferenceCountedOpenSslEngine openSslEngine)) {
+ throw new IllegalArgumentException("BoringSslServerKeyManager requires netty's OpenSSL provider");
+ }
+
+ @Nullable final String[] peerSignatureAlgorithms = SSL.getSigAlgs(openSslEngine.sslPointer());
+ if (peerSignatureAlgorithms == null) {
+ return null;
+ }
+
+ for (final String signatureAlgorithm : peerSignatureAlgorithms) {
+ final Optional maybeAlias = getJdkKeyType(signatureAlgorithm)
+ // The Netty OpenSSL engine doesn't advertise support for Ed25519, and so we can't include the engine in the
+ // delegate's selection process. Instead, we loop through the peer signature algorithms and find a matching key.
+ .map(jdkKeyType -> delegate.chooseServerAlias(jdkKeyType, issuers, null));
+
+ if (maybeAlias.isPresent()) {
+ return maybeAlias.get();
+ }
+ }
+
+ return null;
+ }
+
+ /// Maps a [BoringSSL signature algorithm name](https://github.com/google/boringssl/blob/8525ff3/ssl/ssl_privkey.cc#L420-L433)
+ /// to the JDK key type that can produce it
+ @VisibleForTesting
+ static Optional getJdkKeyType(final String signatureAlgorithm) {
+ if (signatureAlgorithm.equals("ed25519")) {
+ return Optional.of("EdDSA");
+ } else if (signatureAlgorithm.startsWith("ecdsa_")) {
+ return Optional.of("EC");
+ } else if (signatureAlgorithm.startsWith("rsa_pss_rsae_")) {
+ return Optional.of("RSA");
+ }
+
+ // We don't need to be exhaustive, because we require TLS 1.3 and only use a limited number of key types.
+ return Optional.empty();
+ }
+
+ @Override
+ public String chooseServerAlias(final String keyType, final Principal[] issuers, final Socket socket) {
+ return delegate.chooseServerAlias(keyType, issuers, socket);
+ }
+
+ @Override
+ public String[] getServerAliases(final String keyType, final Principal[] issuers) {
+ return delegate.getServerAliases(keyType, issuers);
+ }
+
+ @Override
+ public String chooseClientAlias(final String[] keyTypes, final Principal[] issuers, final Socket socket) {
+ return delegate.chooseClientAlias(keyTypes, issuers, socket);
+ }
+
+ @Override
+ public String[] getClientAliases(final String keyType, final Principal[] issuers) {
+ return delegate.getClientAliases(keyType, issuers);
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(final String alias) {
+ return delegate.getCertificateChain(alias);
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(final String alias) {
+ return delegate.getPrivateKey(alias);
+ }
+}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/SniMapper.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/SniMapper.java
index cb5d9299d..8e54d9ad7 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/SniMapper.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/SniMapper.java
@@ -7,8 +7,10 @@ package org.whispersystems.textsecuregcm.grpc.net;
import com.google.common.annotations.VisibleForTesting;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
+import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
import io.netty.util.Mapping;
import java.io.FileInputStream;
import java.io.IOException;
@@ -49,25 +51,35 @@ public class SniMapper {
///
/// @param keyStorePath The path to the [KeyStore]
/// @param keyStorePassword The password for the keyStore
+ /// @param sslProvider The TLS implementation to use
/// @return A [Mapping] that maps domains to the corresponding [SslContext] containing the certificates for that
/// domain
- public static Mapping buildSniMapping(final String keyStorePath, final String keyStorePassword)
- throws IOException {
+ public static Mapping buildSniMapping(final String keyStorePath, final String keyStorePassword,
+ final SslProvider sslProvider) throws IOException {
try (final FileInputStream fis = new FileInputStream(keyStorePath)) {
- return buildSniMapping(fis, keyStorePassword);
+ return buildSniMapping(fis, keyStorePassword, sslProvider);
}
}
@VisibleForTesting
- static Mapping buildSniMapping(final InputStream keyStore, final String keyStorePassword)
- throws IOException {
+ static Mapping buildSniMapping(final InputStream keyStore, final String keyStorePassword,
+ final SslProvider sslProvider) throws IOException {
+
+ if (sslProvider != SslProvider.JDK && sslProvider != SslProvider.OPENSSL) {
+ throw new IllegalArgumentException("Unsupported SSL provider: " + sslProvider);
+ }
+
+ if (sslProvider == SslProvider.OPENSSL) {
+ OpenSsl.ensureAvailability();
+ }
+
try {
final Map domainKeyStores = partitionByDomain(keyStore, keyStorePassword.toCharArray());
final Map sslContextsByDomain = new HashMap<>();
for (final Map.Entry entry : domainKeyStores.entrySet()) {
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(entry.getValue(), keyStorePassword.toCharArray());
- sslContextsByDomain.put(entry.getKey(), buildSslContext(kmf));
+ sslContextsByDomain.put(entry.getKey(), buildSslContext(kmf, sslProvider));
}
// Netty expects the SNI mapping to always return an SslContext. Per RFC-6066 it's valid to continue the handshake
@@ -78,15 +90,23 @@ public class SniMapper {
.orElseThrow(() -> new IllegalArgumentException("Key store contained no certificates"))
.getValue();
- logger.info("Loaded TLS contexts for domains: {}", sslContextsByDomain.keySet());
+ logger.info("Loaded {} TLS contexts for domains: {}", sslProvider, sslContextsByDomain.keySet());
return hostname -> sslContextsByDomain.getOrDefault(hostname, defaultSslContext);
} catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | UnrecoverableKeyException e) {
throw new IOException("Failed to load keystore", e);
}
}
- private static SslContext buildSslContext(final KeyManagerFactory kmf) throws SSLException {
- return SslContextBuilder.forServer(kmf)
+ private static SslContext buildSslContext(final KeyManagerFactory kmf, final SslProvider sslProvider)
+ throws SSLException {
+
+ final SslContextBuilder sslContextBuilder = sslProvider == SslProvider.OPENSSL
+ // work around netty's OpenSSL provider not selecting Ed25519 certificates on its own
+ ? SslContextBuilder.forServer(BoringSslServerKeyManager.wrap(kmf.getKeyManagers()))
+ : SslContextBuilder.forServer(kmf);
+
+ return sslContextBuilder
+ .sslProvider(sslProvider)
.applicationProtocolConfig(new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/BoringSslServerKeyManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/BoringSslServerKeyManagerTest.java
new file mode 100644
index 000000000..8a4047b49
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/BoringSslServerKeyManagerTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+package org.whispersystems.textsecuregcm.grpc.net;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class BoringSslServerKeyManagerTest {
+
+ @ParameterizedTest
+ @CsvSource(nullValues = "null", value = {
+ "ed25519, EdDSA",
+ "ecdsa_sha256, EC",
+ "ecdsa_secp256r1_sha256, EC",
+ "rsa_pss_rsae_sha256, RSA",
+ "rsa_pss_rsae_sha512, RSA",
+ "rsa_pkcs1_sha256, null",
+ "rsa_pss_pss_sha256, null",
+ "ed448, null",
+ })
+ void getJdkKeyType(final String boringSslSignatureAlgorithm, @Nullable final String expectedKeyType) {
+ assertEquals(Optional.ofNullable(expectedKeyType), BoringSslServerKeyManager.getJdkKeyType(boringSslSignatureAlgorithm));
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/OmnibusH2ServerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/OmnibusH2ServerTest.java
index df0437d78..438fecd6f 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/OmnibusH2ServerTest.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/OmnibusH2ServerTest.java
@@ -51,6 +51,7 @@ import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.pkitesting.CertificateBuilder;
import io.netty.util.Mapping;
@@ -142,7 +143,7 @@ class OmnibusH2ServerTest {
.store(byteArrayOutputStream, keyStorePassword);
sniMapping = SniMapper.buildSniMapping(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()),
- new String(keyStorePassword));
+ new String(keyStorePassword), SslProvider.OPENSSL);
nioEventLoopGroup = new NioEventLoopGroup();
localEventLoopGroup = new DefaultEventLoopGroup();
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/SniMapperTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/SniMapperTest.java
index 2fbf50fe3..a43689d67 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/SniMapperTest.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/SniMapperTest.java
@@ -5,7 +5,9 @@
package org.whispersystems.textsecuregcm.grpc.net;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.netty.bootstrap.Bootstrap;
@@ -23,6 +25,7 @@ import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
+import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.pkitesting.CertificateBuilder;
import io.netty.pkitesting.X509Bundle;
@@ -33,11 +36,16 @@ import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
+import java.util.EnumMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
@@ -45,10 +53,9 @@ import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
class SniMapperTest {
@@ -63,7 +70,8 @@ class SniMapperTest {
};
private static DefaultEventLoopGroup eventLoopGroup;
- private static Mapping sniMapping;
+ private static final Map> sniMappingsByProvider =
+ new EnumMap<>(SslProvider.class);
private Channel serverChannel;
@@ -96,14 +104,16 @@ class SniMapperTest {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
keyStore.store(byteArrayOutputStream, keyStorePassword);
- sniMapping = SniMapper.buildSniMapping(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()),
- new String(keyStorePassword));
+ for (final SslProvider sslProvider : new SslProvider[] { SslProvider.JDK, SslProvider.OPENSSL }) {
+ sniMappingsByProvider.put(sslProvider, SniMapper.buildSniMapping(
+ new ByteArrayInputStream(byteArrayOutputStream.toByteArray()), new String(keyStorePassword), sslProvider));
+ }
eventLoopGroup = new DefaultEventLoopGroup();
}
- @BeforeEach
- void setUp() throws Exception {
+ private void startServer(final SslProvider sslProvider) throws InterruptedException {
+ final Mapping sniMapping = sniMappingsByProvider.get(sslProvider);
final LocalAddress localAddress = new LocalAddress(SniMapper.class.getSimpleName());
serverChannel = new ServerBootstrap()
.group(eventLoopGroup)
@@ -131,9 +141,11 @@ class SniMapperTest {
eventLoopGroup.shutdownGracefully(1, 1000, TimeUnit.MILLISECONDS).sync();
}
- @Test
- void unknownDomain() throws Exception {
- assertNotNull(sniMapping.map("unknown.example.com"));
+ @ParameterizedTest
+ @EnumSource(value = SslProvider.class, names = {"JDK", "OPENSSL"})
+ void unknownDomain(final SslProvider sslProvider) throws Exception {
+ assertNotNull(sniMappingsByProvider.get(sslProvider).map("unknown.example.com"));
+ startServer(sslProvider);
final X509Certificate defaultCertificate = connectAndGetServerCertificate("unknown.example.com", null);
// bar.example.com is the lexicographically first domain, so we should default to it.
@@ -141,7 +153,7 @@ class SniMapperTest {
}
static List selectCertificate() {
- return List.of(
+ final List cases = List.of(
Arguments.of(FOO_DOMAIN, List.of(), "Ed25519"),
Arguments.of(BAR_DOMAIN, List.of(), "Ed25519"),
Arguments.of(BAR_DOMAIN, List.of("ed25519"), "Ed25519"),
@@ -149,21 +161,44 @@ class SniMapperTest {
Arguments.of(FOO_DOMAIN, List.of("rsa_pss_rsae_sha256", "rsa_pss_rsae_sha384", "rsa_pss_rsae_sha512", "rsa_pkcs1_sha256", "ed25519"), "SHA256withRSA"),
Arguments.of(FOO_DOMAIN, List.of("ed25519", "rsa_pss_rsae_sha256", "rsa_pss_rsae_sha384", "rsa_pss_rsae_sha512"), "Ed25519")
);
+
+ // Each provider should select the same certificate
+ return Stream.of(SslProvider.JDK, SslProvider.OPENSSL)
+ .flatMap(sslProvider -> cases.stream().map(arguments -> {
+ final Object[] args = arguments.get();
+ return Arguments.of(sslProvider, args[0], args[1], args[2]);
+ }))
+ .toList();
}
@ParameterizedTest
@MethodSource
- void selectCertificate(final String sni, final List signatureSchemes, final String expectedSigAlgorithm)
- throws Exception {
+ void selectCertificate(final SslProvider sslProvider, final String sni, final List signatureSchemes,
+ final String expectedSigAlgorithm) throws Exception {
+ startServer(sslProvider);
final X509Certificate serverCert = connectAndGetServerCertificate(sni, signatureSchemes.toArray(String[]::new));
assertNotNull(serverCert);
assertCertificateIsForDomain(serverCert, sni);
assertEquals(expectedSigAlgorithm, serverCert.getSigAlgName());
}
+ @ParameterizedTest
+ @EnumSource(value = SslProvider.class, names = {"JDK", "OPENSSL"})
+ void noCommonSignatureAlgorithm(final SslProvider sslProvider) throws Exception {
+ startServer(sslProvider);
+
+ final ExecutionException executionException = assertThrows(ExecutionException.class,
+ () -> connectAndGetServerCertificate(FOO_DOMAIN, new String[] { "ecdsa_secp256r1_sha256" }),
+ "server doesn’t have an ECDSA key");
+
+ assertInstanceOf(SSLException.class, executionException.getCause());
+ }
+
private X509Certificate connectAndGetServerCertificate(final String sniHostname,
final String[] signatureSchemes) throws Exception {
final SslContext clientSsl = SslContextBuilder.forClient()
+ // the client can always use the JDK provider
+ .sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.protocols("TLSv1.3")
.build();
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/TlsHandshakeMetricsHandlerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/TlsHandshakeMetricsHandlerTest.java
index ab85e8a7e..684a73102 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/TlsHandshakeMetricsHandlerTest.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/net/TlsHandshakeMetricsHandlerTest.java
@@ -51,7 +51,8 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
class TlsHandshakeMetricsHandlerTest {
@@ -114,9 +115,10 @@ class TlsHandshakeMetricsHandlerTest {
eventLoopGroup.shutdownGracefully(0, 1000, TimeUnit.MILLISECONDS).sync();
}
- @Test
- void handshake() throws Exception {
- final AtomicReference lastServerChildChannel = startServer(buildSniMapping());
+ @ParameterizedTest
+ @EnumSource(value = SslProvider.class, names = {"JDK", "OPENSSL"})
+ void handshake(final SslProvider sslProvider) throws Exception {
+ final AtomicReference lastServerChildChannel = startServer(buildSniMapping(sslProvider));
connect(null);
assertEquals(1, getCount(true));
@@ -125,9 +127,11 @@ class TlsHandshakeMetricsHandlerTest {
assertNull(lastServerChildChannel.get().pipeline().get(TlsHandshakeMetricsHandler.class), "the metrics handler should remove itself");
}
- @Test
- void failedHandshake() throws Exception {
- startServer(buildSniMapping());
+ @ParameterizedTest
+ @EnumSource(value = SslProvider.class, names = {"JDK", "OPENSSL"})
+ void failedHandshake(final SslProvider sslProvider) throws Exception {
+ startServer(buildSniMapping(sslProvider));
+
// The server only holds an Ed25519 key
connect(new String[] { "rsa_pss_rsae_sha256" });
@@ -136,8 +140,8 @@ class TlsHandshakeMetricsHandlerTest {
assertEquals(1, getCount(false));
}
- private static Mapping buildSniMapping() throws Exception {
- return SniMapper.buildSniMapping(new ByteArrayInputStream(keyStoreBytes), keyStorePassword);
+ private static Mapping buildSniMapping(final SslProvider sslProvider) throws Exception {
+ return SniMapper.buildSniMapping(new ByteArrayInputStream(keyStoreBytes), keyStorePassword, sslProvider);
}
/// Starts the server and returns a reference to the child channel, for introspection by tests