Skip to content

Commit c20187b

Browse files
New BoringSSL context option - SSL_CTX_set1_sigalgs (#765)
My use case of this library had the requirement to use a different signing algorithm and certificate type (ed25519 to be specific). Changes: - New BoringSSL context option - SSL_CTX_set1_sigalgs (this expands on #755 ) - Allowing certificate types in certificate callback to be modified (allows the implementer to override the defaults and use any certificate type they want now) Result: This increases the overall flexibility of the quic implementation. --------- Co-authored-by: Norman Maurer <norman_maurer@apple.com>
1 parent 5adb02e commit c20187b

File tree

6 files changed

+215
-18
lines changed

6 files changed

+215
-18
lines changed

codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,22 @@ static int SSLContext_set1_groups_list(long ctx, String... groups) {
102102
return SSLContext_set1_groups_list(ctx, sb.toString());
103103
}
104104

105+
static int SSLContext_set1_sigalgs_list(long ctx, String... sigalgs) {
106+
if (sigalgs.length == 0) {
107+
throw new IllegalArgumentException();
108+
}
109+
StringBuilder sb = new StringBuilder();
110+
for (String sigalg: sigalgs) {
111+
sb.append(sigalg);
112+
// Groups are separated by : as explained in the manpage.
113+
sb.append(':');
114+
}
115+
sb.setLength(sb.length() - 1);
116+
return SSLContext_set1_sigalgs_list(ctx, sb.toString());
117+
}
118+
119+
private static native int SSLContext_set1_sigalgs_list(long context, String sigalgs);
120+
105121
private static native int SSLContext_set1_groups_list(long context, String groups);
106122
static native void SSLContext_free(long context);
107123
static long SSL_new(long context, boolean server, String hostname) {

codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLCertificateCallback.java

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,18 @@ final class BoringSSLCertificateCallback {
6666
static final String KEY_TYPE_EC_RSA = "EC_RSA";
6767

6868
// key type mappings for types.
69-
private static final Map<String, String> KEY_TYPES = new HashMap<String, String>();
69+
private static final Map<String, String> DEFAULT_SERVER_KEY_TYPES = new HashMap<String, String>();
7070
static {
71-
KEY_TYPES.put("RSA", KEY_TYPE_RSA);
72-
KEY_TYPES.put("DHE_RSA", KEY_TYPE_RSA);
73-
KEY_TYPES.put("ECDHE_RSA", KEY_TYPE_RSA);
74-
KEY_TYPES.put("ECDHE_ECDSA", KEY_TYPE_EC);
75-
KEY_TYPES.put("ECDH_RSA", KEY_TYPE_EC_RSA);
76-
KEY_TYPES.put("ECDH_ECDSA", KEY_TYPE_EC_EC);
77-
KEY_TYPES.put("DH_RSA", KEY_TYPE_DH_RSA);
71+
DEFAULT_SERVER_KEY_TYPES.put("RSA", KEY_TYPE_RSA);
72+
DEFAULT_SERVER_KEY_TYPES.put("DHE_RSA", KEY_TYPE_RSA);
73+
DEFAULT_SERVER_KEY_TYPES.put("ECDHE_RSA", KEY_TYPE_RSA);
74+
DEFAULT_SERVER_KEY_TYPES.put("ECDHE_ECDSA", KEY_TYPE_EC);
75+
DEFAULT_SERVER_KEY_TYPES.put("ECDH_RSA", KEY_TYPE_EC_RSA);
76+
DEFAULT_SERVER_KEY_TYPES.put("ECDH_ECDSA", KEY_TYPE_EC_EC);
77+
DEFAULT_SERVER_KEY_TYPES.put("DH_RSA", KEY_TYPE_DH_RSA);
7878
}
7979

80-
private static final Set<String> SUPPORTED_KEY_TYPES = Collections.unmodifiableSet(new LinkedHashSet<>(
80+
private static final Set<String> DEFAULT_CLIENT_KEY_TYPES = Collections.unmodifiableSet(new LinkedHashSet<>(
8181
Arrays.asList(KEY_TYPE_RSA,
8282
KEY_TYPE_DH_RSA,
8383
KEY_TYPE_EC,
@@ -90,11 +90,16 @@ final class BoringSSLCertificateCallback {
9090
private final QuicheQuicSslEngineMap engineMap;
9191
private final X509ExtendedKeyManager keyManager;
9292
private final String password;
93+
private final Map<String, String> serverKeyTypes;
94+
private final Set<String> clientKeyTypes;
9395

94-
BoringSSLCertificateCallback(QuicheQuicSslEngineMap engineMap, @Nullable X509ExtendedKeyManager keyManager, String password) {
96+
BoringSSLCertificateCallback(QuicheQuicSslEngineMap engineMap, @Nullable X509ExtendedKeyManager keyManager, String password, Map<String, String> serverKeyTypes, Set<String> clientKeyTypes) {
9597
this.engineMap = engineMap;
9698
this.keyManager = keyManager;
9799
this.password = password;
100+
101+
this.serverKeyTypes = serverKeyTypes != null ? serverKeyTypes : DEFAULT_SERVER_KEY_TYPES;
102+
this.clientKeyTypes = clientKeyTypes != null ? clientKeyTypes : DEFAULT_CLIENT_KEY_TYPES;
98103
}
99104

100105
@SuppressWarnings("unused")
@@ -154,9 +159,9 @@ final class BoringSSLCertificateCallback {
154159
// authMethods may contain duplicates or may result in the same type
155160
// but call chooseServerAlias(...) may be expensive. So let's ensure
156161
// we filter out duplicates.
157-
Set<String> typeSet = new HashSet<String>(KEY_TYPES.size());
162+
Set<String> typeSet = new HashSet<String>(serverKeyTypes.size());
158163
for (String authMethod : authMethods) {
159-
String type = KEY_TYPES.get(authMethod);
164+
String type = serverKeyTypes.get(authMethod);
160165
if (type != null && typeSet.add(type)) {
161166
String alias = chooseServerAlias(engine, type);
162167
if (alias != null) {
@@ -243,10 +248,10 @@ private String chooseServerAlias(QuicheQuicSslEngine engine, String type) {
243248
* @return supported key types that can be used in {@code X509KeyManager.chooseClientAlias} and
244249
* {@code X509ExtendedKeyManager.chooseEngineClientAlias}.
245250
*/
246-
private static Set<String> supportedClientKeyTypes(byte @Nullable[] clientCertificateTypes) {
251+
private Set<String> supportedClientKeyTypes(byte @Nullable[] clientCertificateTypes) {
247252
if (clientCertificateTypes == null) {
248253
// Try all of the supported key types.
249-
return SUPPORTED_KEY_TYPES;
254+
return clientKeyTypes;
250255
}
251256
Set<String> result = new HashSet<>(clientCertificateTypes.length);
252257
for (byte keyTypeCode : clientCertificateTypes) {

codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLContextOption.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
import io.netty.handler.ssl.SslContextOption;
1919

20+
import java.util.Map;
21+
import java.util.Set;
22+
2023
/**
2124
* {@link SslContextOption}s that are specific to BoringSSL.
2225
*
@@ -34,4 +37,22 @@ private BoringSSLContextOption(String name) {
3437
* SSL_CTX_set1_groups_list</a>.
3538
*/
3639
public static final BoringSSLContextOption<String[]> GROUPS = new BoringSSLContextOption<>("GROUPS");
40+
41+
/**
42+
* Set the signature algorithms that should be used.
43+
* <p>
44+
* See <a href="https://github.com/google/boringssl/blob/master/include/openssl/ssl.h#L5166">
45+
* SSL_CTX_set1_sigalgs</a>.
46+
*/
47+
public static final BoringSSLContextOption<String[]> SIGNATURE_ALGORITHMS = new BoringSSLContextOption<>("SIGNATURE_ALGORITHMS");
48+
49+
/**
50+
* Set the supported client key/certificate types used in BoringSSLCertificateCallback
51+
*/
52+
public static final BoringSSLContextOption<Set<String>> CLIENT_KEY_TYPES = new BoringSSLContextOption<>("CLIENT_KEY_TYPES");
53+
54+
/**
55+
* Set the supported server key/certificate types used in BoringSSLCertificateCallback
56+
*/
57+
public static final BoringSSLContextOption<Map<String, String>> SERVER_KEY_TYPES = new BoringSSLContextOption<>("SERVER_KEY_TYPES");
3758
}

codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ final class QuicheQuicSslContext extends QuicSslContext {
186186
keyManager = chooseKeyManager(keyManagerFactory);
187187
}
188188
String[] groups = NAMED_GROUPS;
189+
String[] sigalgs = EmptyArrays.EMPTY_STRINGS;
190+
Map<String, String> serverKeyTypes = null;
191+
Set<String> clientKeyTypes = null;
192+
189193
if (ctxOptions != null) {
190194
for (Map.Entry<SslContextOption<?>, Object> ctxOpt : ctxOptions) {
191195
SslContextOption<?> option = ctxOpt.getKey();
@@ -197,6 +201,17 @@ final class QuicheQuicSslContext extends QuicSslContext {
197201
groupsSet.add(GroupsConverter.toBoringSSL(group));
198202
}
199203
groups = groupsSet.toArray(EmptyArrays.EMPTY_STRINGS);
204+
} else if (option == BoringSSLContextOption.SIGNATURE_ALGORITHMS) {
205+
String[] sigalgsArray = (String[]) ctxOpt.getValue();
206+
Set<String> sigalgsSet = new LinkedHashSet<String>(sigalgsArray.length);
207+
for (String sigalg : sigalgsArray) {
208+
sigalgsSet.add(sigalg);
209+
}
210+
sigalgs = sigalgsSet.toArray(EmptyArrays.EMPTY_STRINGS);
211+
} else if (option == BoringSSLContextOption.CLIENT_KEY_TYPES) {
212+
clientKeyTypes = (Set<String>) ctxOpt.getValue();
213+
} else if (option == BoringSSLContextOption.SERVER_KEY_TYPES) {
214+
serverKeyTypes = (Map<String, String>) ctxOpt.getValue();
200215
} else {
201216
LOGGER.debug("Skipping unsupported " + SslContextOption.class.getSimpleName()
202217
+ ": " + ctxOpt.getKey());
@@ -214,7 +229,7 @@ final class QuicheQuicSslContext extends QuicSslContext {
214229
int verifyMode = server ? boringSSLVerifyModeForServer(this.clientAuth) : BoringSSL.SSL_VERIFY_PEER;
215230
nativeSslContext = new NativeSslContext(BoringSSL.SSLContext_new(server, applicationProtocols,
216231
new BoringSSLHandshakeCompleteCallback(engineMap),
217-
new BoringSSLCertificateCallback(engineMap, keyManager, password),
232+
new BoringSSLCertificateCallback(engineMap, keyManager, password, serverKeyTypes, clientKeyTypes),
218233
new BoringSSLCertificateVerifyCallback(engineMap, trustManager),
219234
mapping == null ? null : new BoringSSLTlsextServernameCallback(engineMap, mapping),
220235
keylog == null ? null : new BoringSSLKeylogCallback(engineMap, keylog),
@@ -233,6 +248,16 @@ final class QuicheQuicSslContext extends QuicSslContext {
233248
throw new IllegalStateException(msg);
234249
}
235250

251+
if (sigalgs.length > 0 && BoringSSL.SSLContext_set1_sigalgs_list(nativeSslContext.ctx, sigalgs) == 0) {
252+
String msg = "failed to set signature algorithm list: " + Arrays.toString(sigalgs);
253+
String lastError = BoringSSL.ERR_last_error();
254+
if (lastError != null) {
255+
// We have some more details about why the operations failed, include these into the message.
256+
msg += ". " + lastError;
257+
}
258+
throw new IllegalStateException(msg);
259+
}
260+
236261
apn = new QuicheQuicApplicationProtocolNegotiator(applicationProtocols);
237262
if (this.sessionCache != null) {
238263
// Cache is handled via our own implementation.

codec-native-quic/src/main/c/netty_quic_boringssl.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,13 @@ jint netty_boringssl_SSLContext_set1_groups_list(JNIEnv* env, jclass clazz, jlon
14271427
return (jint) ret;
14281428
}
14291429

1430+
jint netty_boringssl_SSLContext_set1_sigalgs_list(JNIEnv* env, jclass clazz, jlong ctx, jstring sigalgs) {
1431+
const char *nativeString = (*env)->GetStringUTFChars(env, sigalgs, 0);
1432+
int ret = SSL_CTX_set1_sigalgs_list((SSL_CTX *) ctx, nativeString);
1433+
(*env)->ReleaseStringUTFChars(env, sigalgs, nativeString);
1434+
return (jint) ret;
1435+
}
1436+
14301437
// JNI Registered Methods End
14311438

14321439
// JNI Method Registration Table Begin
@@ -1464,6 +1471,7 @@ static const JNINativeMethod fixed_method_table[] = {
14641471
{ "SSLContext_set_early_data_enabled", "(JZ)V", (void *) netty_boringssl_SSLContext_set_early_data_enabled },
14651472
{ "SSLContext_setSessionTicketKeys", "(JZ)V", (void *) netty_boringssl_SSLContext_setSessionTicketKeys },
14661473
{ "SSLContext_set1_groups_list", "(JLjava/lang/String;)I", (void *) netty_boringssl_SSLContext_set1_groups_list },
1474+
{ "SSLContext_set1_sigalgs_list", "(JLjava/lang/String;)I", (void *) netty_boringssl_SSLContext_set1_sigalgs_list },
14671475
{ "SSL_new0", "(JZLjava/lang/String;)J", (void *) netty_boringssl_SSL_new0 },
14681476
{ "SSL_free", "(J)V", (void *) netty_boringssl_SSL_free },
14691477
{ "SSL_getTask", "(J)Ljava/lang/Runnable;", (void *) netty_boringssl_SSL_getTask },

codec-native-quic/src/test/java/io/netty/incubator/codec/quic/QuicChannelConnectTest.java

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.netty.channel.ChannelPromise;
2828
import io.netty.channel.ConnectTimeoutException;
2929
import io.netty.channel.socket.ChannelInputShutdownEvent;
30+
import io.netty.handler.codec.ByteToMessageDecoder;
3031
import io.netty.handler.ssl.ClientAuth;
3132
import io.netty.handler.ssl.SniCompletionEvent;
3233
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
@@ -70,9 +71,14 @@
7071
import java.security.spec.MGF1ParameterSpec;
7172
import java.security.spec.PSSParameterSpec;
7273
import java.util.ArrayList;
74+
import java.util.HashMap;
75+
import java.util.HashSet;
7376
import java.util.List;
77+
import java.util.Map;
78+
import java.util.Set;
7479
import java.util.concurrent.BlockingQueue;
7580
import java.util.concurrent.CountDownLatch;
81+
import java.util.concurrent.ExecutionException;
7682
import java.util.concurrent.Executor;
7783
import java.util.concurrent.LinkedBlockingQueue;
7884
import java.util.concurrent.TimeUnit;
@@ -81,13 +87,14 @@
8187
import java.util.function.Consumer;
8288

8389
import static org.hamcrest.MatcherAssert.assertThat;
90+
import static org.junit.jupiter.api.Assertions.assertSame;
91+
import static org.junit.jupiter.api.Assertions.assertTrue;
92+
import static org.junit.jupiter.api.Assertions.assertThrows;
93+
import static org.junit.jupiter.api.Assertions.fail;
8494
import static org.junit.jupiter.api.Assertions.assertEquals;
8595
import static org.junit.jupiter.api.Assertions.assertNotEquals;
8696
import static org.junit.jupiter.api.Assertions.assertNotNull;
8797
import static org.junit.jupiter.api.Assertions.assertNull;
88-
import static org.junit.jupiter.api.Assertions.assertSame;
89-
import static org.junit.jupiter.api.Assertions.assertTrue;
90-
import static org.junit.jupiter.api.Assertions.fail;
9198

9299
public class QuicChannelConnectTest extends AbstractQuicTest {
93100

@@ -729,6 +736,121 @@ public void testConnectWithoutTokenValidation(Executor executor) throws Throwabl
729736
}
730737
}
731738

739+
@ParameterizedTest
740+
@MethodSource("newSslTaskExecutors")
741+
public void testKeyTypeChange(Executor executor) throws Throwable {
742+
final CountDownLatch readLatch = new CountDownLatch(1);
743+
Map<String, String> serverKeyTypes = new HashMap<>();
744+
serverKeyTypes.put("RSA", "RSA");
745+
746+
Set<String> clientKeyTypes = new HashSet<>();
747+
clientKeyTypes.add("RSA");
748+
749+
Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor,
750+
QuicSslContextBuilder.forServer(
751+
QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
752+
QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
753+
.applicationProtocols(QuicTestUtils.PROTOS)
754+
.option(BoringSSLContextOption.SERVER_KEY_TYPES, serverKeyTypes)
755+
.earlyData(true)
756+
.build()),
757+
TestQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() {
758+
@Override
759+
public boolean isSharable() {
760+
return true;
761+
}
762+
}, new ByteToMessageDecoder() {
763+
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
764+
if (msg.readableBytes() < 4) {
765+
return;
766+
}
767+
assertEquals(5, msg.readInt());
768+
readLatch.countDown();
769+
ctx.close();
770+
}
771+
});
772+
773+
InetSocketAddress address = (InetSocketAddress) server.localAddress();
774+
775+
QuicSslContext sslContext = QuicSslContextBuilder.forClient()
776+
.trustManager(InsecureTrustManagerFactory.INSTANCE)
777+
.applicationProtocols(QuicTestUtils.PROTOS)
778+
.option(BoringSSLContextOption.CLIENT_KEY_TYPES, clientKeyTypes)
779+
.earlyData(true)
780+
.build();
781+
782+
Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor, sslContext)
783+
.sslEngineProvider(q -> sslContext.newEngine(q.alloc(), "localhost", 9999)));
784+
785+
try {
786+
QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
787+
.streamHandler(new ChannelInboundHandlerAdapter())
788+
.remoteAddress(address)
789+
.connect()
790+
.get();
791+
quicChannel.createStream(QuicStreamType.BIDIRECTIONAL,
792+
new ChannelInboundHandlerAdapter()).addListener(f -> {
793+
Channel stream = (Channel) f.getNow();
794+
stream.writeAndFlush(stream.alloc().buffer().writeInt(5));
795+
}).await().addListener(f -> {
796+
assertTrue(f.isSuccess());
797+
});
798+
799+
readLatch.await();
800+
} finally {
801+
server.close().sync();
802+
channel.close().sync();
803+
shutdown(executor);
804+
}
805+
}
806+
807+
@ParameterizedTest
808+
@MethodSource("newSslTaskExecutors")
809+
public void testKeyTypeChangeFail(Executor executor) throws Throwable {
810+
Map<String, String> serverKeyTypes = new HashMap<>();
811+
serverKeyTypes.put("ECDHE_ECDSA", "EdDSA");
812+
813+
Set<String> clientKeyTypes = new HashSet<>();
814+
clientKeyTypes.add("EdDSA");
815+
816+
Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor,
817+
QuicSslContextBuilder.forServer(
818+
QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
819+
QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
820+
.applicationProtocols(QuicTestUtils.PROTOS)
821+
.option(BoringSSLContextOption.SERVER_KEY_TYPES, serverKeyTypes)
822+
.earlyData(true)
823+
.build()),
824+
TestQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter(),
825+
new ChannelInboundHandlerAdapter());
826+
827+
InetSocketAddress address = (InetSocketAddress) server.localAddress();
828+
829+
QuicSslContext sslContext = QuicSslContextBuilder.forClient()
830+
.trustManager(InsecureTrustManagerFactory.INSTANCE)
831+
.applicationProtocols(QuicTestUtils.PROTOS)
832+
.option(BoringSSLContextOption.CLIENT_KEY_TYPES, clientKeyTypes)
833+
.earlyData(true)
834+
.build();
835+
836+
Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor, sslContext)
837+
.sslEngineProvider(q -> sslContext.newEngine(q.alloc(), "localhost", 9999)));
838+
839+
try {
840+
assertThrows(ExecutionException.class, ()->{
841+
QuicTestUtils.newQuicChannelBootstrap(channel)
842+
.streamHandler(new ChannelInboundHandlerAdapter())
843+
.remoteAddress(address)
844+
.connect()
845+
.get();
846+
});
847+
} finally {
848+
server.close().sync();
849+
channel.close().sync();
850+
shutdown(executor);
851+
}
852+
}
853+
732854
@ParameterizedTest
733855
@MethodSource("newSslTaskExecutors")
734856
public void testConnectWith0RTT(Executor executor) throws Throwable {

0 commit comments

Comments
 (0)