mirror of
https://github.com/signalapp/Signal-Server
synced 2026-10-06 11:07:51 +01:00
Add support for BoringSSL provider via netty-tcnative
This commit is contained in:
1 parent
60738acd17
commit
e90c8685f8
10 files changed
+274
-40
No files matched your search
@@ -78,6 +78,8 @@
|
||||
<luajava.version>4.1.0</luajava.version>
|
||||
<micrometer.version>1.17.1</micrometer.version>
|
||||
<netty.version>4.2.18.Final</netty.version>
|
||||
<!-- netty-bom doesn't manage the BoringSSL uber-jar; keep in sync with netty's tcnative.version -->
|
||||
<netty-tcnative.version>2.0.84.Final</netty-tcnative.version>
|
||||
<!-- must be greater than or equal to the value from Google libraries-bom, see https://protobuf.dev/support/cross-version-runtime-guarantee/ -->
|
||||
<!-- the latest 4.x should be fine https://central.sonatype.com/artifact/com.google.protobuf/protobuf-java/versions -->
|
||||
<protobuf.version>4.36.0</protobuf.version>
|
||||
@@ -141,6 +143,11 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-tcnative-boringssl-static</artifactId>
|
||||
<version>${netty-tcnative.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-bom</artifactId>
|
||||
|
||||
@@ -513,6 +513,11 @@
|
||||
<artifactId>netty-resolver-dns</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-tcnative-boringssl-static</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-transport</artifactId>
|
||||
@@ -795,6 +800,7 @@
|
||||
<jvmFlag>-Djdk.nio.maxCachedBufferSize=262144</jvmFlag>
|
||||
<jvmFlag>-Dlog4j2.formatMsgNoLookups=true</jvmFlag>
|
||||
<jvmFlag>-Djdk.tls.server.newSessionTicketCount=0</jvmFlag>
|
||||
<jvmFlag>-Djdk.tls.server.enableSessionTicketExtension=false</jvmFlag>
|
||||
<jvmFlag>-XX:MaxRAMPercentage=75</jvmFlag>
|
||||
<jvmFlag>-XX:+HeapDumpOnOutOfMemoryError</jvmFlag>
|
||||
<jvmFlag>-XX:HeapDumpPath=/tmp/heapdump.bin</jvmFlag>
|
||||
|
||||
@@ -666,7 +666,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
final ScheduledExecutorService retryExecutor = ScheduledExecutorServiceBuilder.of(environment, "retry")
|
||||
.threads(16).build();
|
||||
final ScheduledExecutorService registrationIdentityTokenRefreshExecutor =
|
||||
ScheduledExecutorServiceBuilder.of(environment, "registrationIdentityTokenRefresh").threads(1).build();
|
||||
ScheduledExecutorServiceBuilder.of(environment, "registrationIdentityTokenRefresh").threads(1).build();
|
||||
final ScheduledExecutorService presenceRenewalExecutor =
|
||||
ScheduledExecutorServiceBuilder.of(environment, "presenceRenewal").threads(4).build();
|
||||
|
||||
@@ -726,11 +726,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
final ManagedEventLoopGroup<NioEventLoopGroup> 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<WhisperServerConfiguration
|
||||
grpcLocalAddress);
|
||||
@Nullable final Mapping<String, SslContext> 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(),
|
||||
|
||||
+7
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, SslContext> buildSniMapping(final String keyStorePath, final String keyStorePassword)
|
||||
throws IOException {
|
||||
public static Mapping<String, SslContext> 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<String, SslContext> buildSniMapping(final InputStream keyStore, final String keyStorePassword)
|
||||
throws IOException {
|
||||
static Mapping<String, SslContext> 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<String, KeyStore> domainKeyStores = partitionByDomain(keyStore, keyStorePassword.toCharArray());
|
||||
final Map<String, SslContext> sslContextsByDomain = new HashMap<>();
|
||||
for (final Map.Entry<String, KeyStore> 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,
|
||||
|
||||
+30
@@ -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));
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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();
|
||||
|
||||
+48
-13
@@ -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<String, SslContext> sniMapping;
|
||||
private static final Map<SslProvider, Mapping<String, SslContext>> 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<String, SslContext> 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<Arguments> selectCertificate() {
|
||||
return List.of(
|
||||
final List<Arguments> 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<String> signatureSchemes, final String expectedSigAlgorithm)
|
||||
throws Exception {
|
||||
void selectCertificate(final SslProvider sslProvider, final String sni, final List<String> 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();
|
||||
|
||||
+13
-9
@@ -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<Channel> lastServerChildChannel = startServer(buildSniMapping());
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = SslProvider.class, names = {"JDK", "OPENSSL"})
|
||||
void handshake(final SslProvider sslProvider) throws Exception {
|
||||
final AtomicReference<Channel> 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<String, SslContext> buildSniMapping() throws Exception {
|
||||
return SniMapper.buildSniMapping(new ByteArrayInputStream(keyStoreBytes), keyStorePassword);
|
||||
private static Mapping<String, SslContext> 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
|
||||
|
||||
Reference in new issue
Block a user