diff --git a/.github/workflows/ci-al4.yml b/.github/workflows/ci-al4.yml index ca1e74f8f7ad..0f7b4bd94f09 100644 --- a/.github/workflows/ci-al4.yml +++ b/.github/workflows/ci-al4.yml @@ -49,7 +49,7 @@ jobs: git config --global --add safe.directory /__w/CCF/CCF mkdir build cd build - cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DTEST_HYBRID_TLS_GROUPS=ON .. + cmake -GNinja -DCMAKE_BUILD_TYPE=Debug .. ninja shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cf14b23386b..07e6dfa7f1d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,12 +134,6 @@ option(LONG_TESTS "Enable long end-to-end tests" OFF) option(USE_SNMALLOC "Link against snmalloc" ON) include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/snmalloc.cmake) -option( - TEST_HYBRID_TLS_GROUPS - "Assert that TLS handshakes negotiate a hybrid post-quantum group, which requires OpenSSL 3.5 or later" - OFF -) - option(CLANG_TIDY "Run clang-tidy on the codebase" OFF) option( @@ -881,10 +875,6 @@ if(BUILD_TESTS) add_unit_test(tls_test ${CMAKE_CURRENT_SOURCE_DIR}/src/tls/test/main.cpp) target_link_libraries(tls_test PRIVATE ${CMAKE_THREAD_LIBS_INIT}) - target_compile_definitions( - tls_test - PRIVATE TEST_HYBRID_TLS_GROUPS=$ - ) add_unit_test( base64_test @@ -1360,13 +1350,11 @@ if(BUILD_TESTS) BUCKET bucket_c ) - if(TEST_HYBRID_TLS_GROUPS) - add_e2e_test( - NAME tls_groups_test - PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/tls_groups.py - BUCKET bucket_a - ) - endif() + add_e2e_test( + NAME tls_groups_test + PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/tls_groups.py + BUCKET bucket_a + ) if(CLIENT_PROTOCOLS_TEST) add_e2e_test( diff --git a/src/tls/test/main.cpp b/src/tls/test/main.cpp index e36106e4998c..3436ca481890 100644 --- a/src/tls/test/main.cpp +++ b/src/tls/test/main.cpp @@ -526,10 +526,6 @@ class InspectableServer : public tls::Server } }; -#ifndef TEST_HYBRID_TLS_GROUPS -# define TEST_HYBRID_TLS_GROUPS 0 -#endif - // Hybrid groups offered by src/tls/context.h, in the order they are offered constexpr auto secp384r1_mlkem1024 = "SecP384r1MLKEM1024"; constexpr auto secp256r1_mlkem768 = "SecP256r1MLKEM768"; @@ -607,8 +603,7 @@ TEST_CASE("group negotiation") } } -TEST_CASE( - "hybrid group negotiation" * doctest::skip(TEST_HYBRID_TLS_GROUPS == 0)) +TEST_CASE("hybrid group negotiation") { const std::vector hybrid_groups = { secp384r1_mlkem1024, secp256r1_mlkem768, x25519_mlkem768}; diff --git a/tests/ci-buckets.txt b/tests/ci-buckets.txt index 7ca731569c23..c21da141b93a 100644 --- a/tests/ci-buckets.txt +++ b/tests/ci-buckets.txt @@ -1,4 +1,5 @@ bucket_a: + tls_groups_test lts_compatibility bucket_b: diff --git a/tests/tls_groups.py b/tests/tls_groups.py index 759c4c14c9ee..7cba3c9006c0 100644 --- a/tests/tls_groups.py +++ b/tests/tls_groups.py @@ -15,11 +15,24 @@ WEAKEST_GROUP = "P-256" WEAKEST_GROUP_REPORTED = "prime256v1" +# TLS NamedGroup IDs reported by OpenSSL 3.3 +# Match IANA Group IDs defined in +# https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8 +GROUP_IDS = { + "23": WEAKEST_GROUP_REPORTED, + "4587": "SecP256r1MLKEM768", + "4588": "X25519MLKEM768", + "4589": "SecP384r1MLKEM1024", +} + # A group CCF does not offer UNSUPPORTED_GROUP = "X448" -# s_client reports hybrid groups by name, and classical groups as a peer key +# s_client output varies between OpenSSL 3.3 and 3.5 NEGOTIATED_GROUP = re.compile(r"^Negotiated TLS1\.3 group: (\S+)$", re.MULTILINE) +TRACED_GROUP_ID = re.compile( + r"extension_type=key_share\(51\),[^\n]*\n" r"\s+NamedGroup: [^\n]*\((\d+)\)" +) PEER_TEMP_KEY = re.compile(r"^Peer Temp Key: ECDH, ([^,]+),", re.MULTILINE) @@ -29,7 +42,7 @@ def negotiate_group(address, groups): group they agreed on, or None if they could not agree. """ completed = subprocess.run( - ["openssl", "s_client", "-connect", address, "-groups", groups], + ["openssl", "s_client", "-trace", "-connect", address, "-groups", groups], input="", capture_output=True, text=True, @@ -42,6 +55,14 @@ def negotiate_group(address, groups): if match is not None and match.group(1) != "": return match.group(1) + for record in output.split("Received TLS Record")[1:]: + if "ServerHello," in record: + match = TRACED_GROUP_ID.search(record) + if match is not None: + group_id = match.group(1) + return GROUP_IDS.get(group_id, group_id) + break + match = PEER_TEMP_KEY.search(output) if match is not None: return match.group(1)