Support setting the minimum acceptable TLS version.
Change-Id: I24775461fb690abdd0a47c5b4e23776c9fe5bfe0
Bug: https://github.com/dart-lang/sdk/issues/54901
Tested: TLS traffic verified in Wireshark, exception behavior verified with a Python test server, property changes verified by unit test
CoreLibraryReviewExempt: dart:io only
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/365664
Reviewed-by: Brian Quinlan <bquinlan@google.com>
Commit-Queue: Brian Quinlan <bquinlan@google.com>
Reviewed-by: Lasse Nielsen <lrn@google.com>
diff --git a/runtime/bin/io_natives.cc b/runtime/bin/io_natives.cc
index 091b616..552d063 100644
--- a/runtime/bin/io_natives.cc
+++ b/runtime/bin/io_natives.cc
@@ -137,6 +137,8 @@
V(SecurityContext_SetTrustedCertificatesBytes, 3) \
V(SecurityContext_TrustBuiltinRoots, 1) \
V(SecurityContext_SetAllowTlsRenegotiation, 2) \
+ V(SecurityContext_SetMinimumProtocolVersion, 2) \
+ V(SecurityContext_GetMinimumProtocolVersion, 1) \
V(SecurityContext_UseCertificateChainBytes, 3) \
V(ServerSocket_Accept, 2) \
V(ServerSocket_CreateBindListen, 7) \
diff --git a/runtime/bin/secure_socket_unsupported.cc b/runtime/bin/secure_socket_unsupported.cc
index 5d5241db..1324d92 100644
--- a/runtime/bin/secure_socket_unsupported.cc
+++ b/runtime/bin/secure_socket_unsupported.cc
@@ -136,6 +136,18 @@
"Secure Sockets unsupported on this platform"));
}
+void FUNCTION_NAME(SecurityContext_SetMinimumProtocolVersion)(
+ Dart_NativeArguments args) {
+ Dart_ThrowException(DartUtils::NewDartArgumentError(
+ "Secure Sockets unsupported on this platform"));
+}
+
+void FUNCTION_NAME(SecurityContext_GetMinimumProtocolVersion)(
+ Dart_NativeArguments args) {
+ Dart_ThrowException(DartUtils::NewDartArgumentError(
+ "Secure Sockets unsupported on this platform"));
+}
+
void FUNCTION_NAME(SecurityContext_UseCertificateChainBytes)(
Dart_NativeArguments args) {
Dart_ThrowException(DartUtils::NewDartArgumentError(
diff --git a/runtime/bin/security_context.cc b/runtime/bin/security_context.cc
index 5fc9444..6af6116 100644
--- a/runtime/bin/security_context.cc
+++ b/runtime/bin/security_context.cc
@@ -827,6 +827,8 @@
SSL_CTX* ctx = SSL_CTX_new(TLS_method());
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, SSLCertContext::CertificateCallback);
SSL_CTX_set_keylog_callback(ctx, SSLCertContext::KeyLogCallback);
+ // If we change the minimum protocol version here, then the documentation
+ // for `SecurityContext.minimumTlsProtocolVersion` must also be changed.
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
SSL_CTX_set_cipher_list(ctx, "HIGH:MEDIUM");
SSLCertContext* context = new SSLCertContext(ctx);
@@ -901,6 +903,32 @@
context->set_allow_tls_renegotiation(allow);
}
+void FUNCTION_NAME(SecurityContext_SetMinimumProtocolVersion)(
+ Dart_NativeArguments args) {
+ SSLCertContext* context = SSLCertContext::GetSecurityContext(args);
+ Dart_Handle protocol_version_handle =
+ ThrowIfError(Dart_GetNativeArgument(args, 1));
+ if (!Dart_IsInteger(protocol_version_handle)) {
+ Dart_ThrowException(DartUtils::NewDartArgumentError(
+ "Non-int argument passed to SetMinimumProtocolVersion"));
+ }
+
+ int protocol_version = DartUtils::GetIntegerValue(protocol_version_handle);
+ if (SSL_CTX_set_min_proto_version(context->context(), protocol_version) ==
+ 0) {
+ Dart_ThrowException(DartUtils::NewDartArgumentError(
+ "Invalid protocol version passed to SetMinimumProtocolVersion"));
+ }
+}
+
+void FUNCTION_NAME(SecurityContext_GetMinimumProtocolVersion)(
+ Dart_NativeArguments args) {
+ SSLCertContext* context = SSLCertContext::GetSecurityContext(args);
+
+ Dart_SetIntegerReturnValue(args,
+ SSL_CTX_get_min_proto_version(context->context()));
+}
+
void FUNCTION_NAME(X509_Der)(Dart_NativeArguments args) {
Dart_SetReturnValue(args, X509Helper::GetDer(args));
}
diff --git a/sdk/lib/_internal/vm/bin/secure_socket_patch.dart b/sdk/lib/_internal/vm/bin/secure_socket_patch.dart
index 67999d7..f6c2bdf 100644
--- a/sdk/lib/_internal/vm/bin/secure_socket_patch.dart
+++ b/sdk/lib/_internal/vm/bin/secure_socket_patch.dart
@@ -227,6 +227,14 @@
bool get allowLegacyUnsafeRenegotiation => _allowLegacyUnsafeRenegotiation;
+ set minimumTlsProtocolVersion(TlsProtocolVersion version) {
+ _setMinimumProtocolVersion(version._version);
+ }
+
+ TlsProtocolVersion get minimumTlsProtocolVersion =>
+ TlsProtocolVersion._fromProtocolVersionConstant(
+ _getMinimumProtocolVersion());
+
@pragma("vm:external-name", "SecurityContext_Allocate")
external void _createNativeContext();
@@ -279,6 +287,10 @@
external void _trustBuiltinRoots();
@pragma("vm:external-name", "SecurityContext_SetAllowTlsRenegotiation")
external void _setAllowTlsRenegotiation(bool allow);
+ @pragma("vm:external-name", "SecurityContext_SetMinimumProtocolVersion")
+ external void _setMinimumProtocolVersion(int version);
+ @pragma("vm:external-name", "SecurityContext_GetMinimumProtocolVersion")
+ external int _getMinimumProtocolVersion();
}
/**
diff --git a/sdk/lib/io/security_context.dart b/sdk/lib/io/security_context.dart
index 571b32c..df6cbac 100644
--- a/sdk/lib/io/security_context.dart
+++ b/sdk/lib/io/security_context.dart
@@ -4,6 +4,32 @@
part of dart.io;
+/// A Transport Layer Security (TLS) version.
+///
+/// Only TLS versions supported by `dart:io` are included.
+class TlsProtocolVersion {
+ /// Transport Layer Security (TLS) Protocol Version 1.2.
+ ///
+ /// See RFC-5246.
+ static const tls1_2 = TlsProtocolVersion._(0x0303);
+
+ /// Transport Layer Security (TLS) Protocol Version 1.3.
+ ///
+ /// See RFC-8446.
+ static const tls1_3 = TlsProtocolVersion._(0x0304);
+
+ final int _version;
+
+ const TlsProtocolVersion._(this._version);
+
+ static TlsProtocolVersion _fromProtocolVersionConstant(int version) =>
+ switch (version) {
+ 0x0303 => tls1_2,
+ 0x0304 => tls1_3,
+ _ => throw ArgumentError.value(version, 'version'),
+ };
+}
+
/// The object containing the certificates to trust when making
/// a secure client connection, and the certificate chain and
/// private key to serve from a secure server.
@@ -170,6 +196,18 @@
/// where it is known to be safe.
abstract bool allowLegacyUnsafeRenegotiation;
+ /// The minimum TLS version to use when establishing a secure connection.
+ ///
+ /// If the peer does not support `minimumTlsProtocolVersion` or later
+ /// then [SecureSocket.connect] will throw a [TlsException].
+ ///
+ /// If the value is changed, it will only affect new connections. Existing
+ /// connections will continue to use the protocol that was negotiated with the
+ /// peer.
+ ///
+ /// The default value is [TlsProtocolVersion.tls1_2].
+ abstract TlsProtocolVersion minimumTlsProtocolVersion;
+
/// Encodes a set of supported protocols for ALPN/NPN usage.
///
/// The [protocols] list is expected to contain protocols in descending order
diff --git a/tests/standalone/io/secure_socket_minimum_tls_protocol_version_test.dart b/tests/standalone/io/secure_socket_minimum_tls_protocol_version_test.dart
new file mode 100644
index 0000000..14d8214
--- /dev/null
+++ b/tests/standalone/io/secure_socket_minimum_tls_protocol_version_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+//
+// OtherResources=certificates/server_chain.pem
+// OtherResources=certificates/server_key.pem
+// OtherResources=certificates/trusted_certs.pem
+//
+// This test does not verify that the value set in `minimumTlsProtocolVersion`
+// appears in the supported versions extension as defined in RPC-8446 4.2.1.
+import "dart:async";
+import 'dart:convert';
+import "dart:io";
+
+import "package:async_helper/async_helper.dart";
+import "package:expect/expect.dart";
+
+late InternetAddress HOST;
+
+String localFile(path) => Platform.script.resolve(path).toFilePath();
+
+SecurityContext serverContext = new SecurityContext()
+ ..useCertificateChain(localFile('certificates/server_chain.pem'))
+ ..usePrivateKey(localFile('certificates/server_key.pem'),
+ password: 'dartdart');
+
+Future<SecureServerSocket> startEchoServer() {
+ return SecureServerSocket.bind(HOST, 0, serverContext).then((server) {
+ server.listen((SecureSocket client) {
+ client.fold<List<int>>(
+ <int>[], (message, data) => message..addAll(data)).then((message) {
+ client.add(message);
+ client.close();
+ });
+ });
+ return server;
+ });
+}
+
+testVersion(SecureServerSocket server, TlsProtocolVersion tlsVersion) async {
+ // NOTE: this test only verifies that `minimumTlsProtocolVersion` does
+ // not cause incorrect behavior when used - the server does *not* actually
+ // verify that the supported versions extension is correctly set.
+ SecurityContext clientContext = new SecurityContext()
+ ..minimumTlsProtocolVersion = tlsVersion
+ ..setTrustedCertificates(localFile('certificates/trusted_certs.pem'));
+
+ await SecureSocket.connect(HOST, server.port, context: clientContext)
+ .then((socket) async {
+ socket.write("Hello server.");
+ socket.close();
+ Expect.isTrue(await utf8.decoder.bind(socket).contains("Hello server."));
+ });
+}
+
+testProperty() {
+ SecurityContext context = new SecurityContext();
+ Expect.equals(TlsProtocolVersion.tls1_2, context.minimumTlsProtocolVersion);
+ context.minimumTlsProtocolVersion = TlsProtocolVersion.tls1_3;
+ Expect.equals(TlsProtocolVersion.tls1_3, context.minimumTlsProtocolVersion);
+}
+
+void main() async {
+ asyncStart();
+ await InternetAddress.lookup("localhost").then((hosts) => HOST = hosts.first);
+ final server = await startEchoServer();
+
+ testProperty();
+ await testVersion(server, TlsProtocolVersion.tls1_2);
+ await testVersion(server, TlsProtocolVersion.tls1_3);
+
+ await server.close();
+ asyncEnd();
+}