diff options
| author | Mounir IDRASSI <[email protected]> | 2026-05-05 12:30:49 +0200 |
|---|---|---|
| committer | Tomas Mraz <[email protected]> | 2026-07-23 17:55:07 +0200 |
| commit | 3e4f7e77208138174ce50f0403ce6b947cb1d509 (patch) | |
| tree | 2b05ff6070b7c8bb9b3f3d19353204039f2caff0 | |
| parent | crypto: use void * in LoongArch SHA shim (diff) | |
| download | openssl-3e4f7e77208138174ce50f0403ce6b947cb1d509.tar.gz openssl-3e4f7e77208138174ce50f0403ce6b947cb1d509.zip | |
Reject unknown TLS 1.3 ServerHello extensions
TLS 1.3 does not permit clients to ignore unknown extensions in ServerHello
or HelloRetryRequest. Add a validation pass for these messages so fully
unknown extension types fail early with unsupported_extension.
Keep the existing ignore behavior for other extension contexts and leave
TLS 1.2 ServerHello processing unchanged. Add TLSProxy coverage for the
TLS 1.3 ServerHello case.
Correct the unknown-extension path in tls_collect_extensions(). Collection
still runs before the new TLS 1.3 validation, and verify_extension()
returns success with thisex == NULL for fully unknown extension types.
The previous idx calculation used invalid pointer arithmetic on NULL in
that path. Map unknown extensions explicitly outside the built-in extension
range instead, preserving the existing ignore semantics while avoiding
undefined behavior.
Update the truncated HRR ECH corruption vector to keep testing malformed
ECH length after the new unknown-extension validation. The old 0xdddd bytes
were only padding to preserve the replacement length, but they now trigger
the unknown-extension check before the ECH parser. Use a known HRR cookie
extension as padding so the test still reaches the intended
SSL_R_LENGTH_MISMATCH path.
Reviewed-by: Andrew Dinh <[email protected]>
Reviewed-by: Daniel Kubec <[email protected]>
Reviewed-by: Tomas Mraz <[email protected]>
MergeDate: Thu Jul 23 15:55:09 2026
(Merged from https://github.com/openssl/openssl/pull/31086)
| -rw-r--r-- | ssl/statem/extensions.c | 57 | ||||
| -rw-r--r-- | ssl/statem/statem_clnt.c | 6 | ||||
| -rw-r--r-- | ssl/statem/statem_local.h | 2 | ||||
| -rw-r--r-- | test/ech_corrupt_test.c | 4 | ||||
| -rw-r--r-- | test/recipes/70-test_sslextension.t | 22 | ||||
| -rw-r--r-- | util/perl/TLSProxy/Message.pm | 3 |
6 files changed, 86 insertions, 8 deletions
diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c index c3ef683d757..753402b84f0 100644 --- a/ssl/statem/extensions.c +++ b/ssl/statem/extensions.c @@ -799,6 +799,62 @@ static int tls_parse_ec_pt_formats(SSL_CONNECTION *s, PACKET *pkt, } /* + * Verify that all extensions in |packet| are known built-in or custom + * extension types. This is used for TLS 1.3 server extension responses where + * unknown extensions are not ignored. + */ +int tls_validate_no_unknown_extensions(SSL_CONNECTION *s, PACKET *packet, + unsigned int context) +{ + PACKET extensions = *packet; + custom_ext_methods *exts = &s->cert->custext; + ENDPOINT role = ENDPOINT_BOTH; + + if ((context & SSL_EXT_CLIENT_HELLO) != 0) { +#ifndef OPENSSL_NO_ECH + if (s->ext.ech.attempted == 1 && s->ext.ech.ch_depth == 1) + role = ENDPOINT_CLIENT; + else + role = ENDPOINT_SERVER; +#else + role = ENDPOINT_SERVER; +#endif + } else if ((context & SSL_EXT_TLS1_2_SERVER_HELLO) != 0) { + role = ENDPOINT_CLIENT; + } + + while (PACKET_remaining(&extensions) > 0) { + unsigned int type; + size_t i; + PACKET extension; + const EXTENSION_DEFINITION *thisext; + + if (!PACKET_get_net_2(&extensions, &type) + || !PACKET_get_length_prefixed_2(&extensions, &extension)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + + for (i = 0, thisext = ext_defs; i < OSSL_NELEM(ext_defs); + i++, thisext++) { + if (type == thisext->type) + break; + } + if (i < OSSL_NELEM(ext_defs)) + continue; + + if (exts != NULL && custom_ext_find(exts, role, type, NULL) != NULL) + continue; + + SSLfatal(s, SSL_AD_UNSUPPORTED_EXTENSION, + SSL_R_UNSOLICITED_EXTENSION); + return 0; + } + + return 1; +} + +/* * Check whether the context defined for an extension |extctx| means whether * the extension is relevant for the current context |thisctx| or not. Returns * 1 if the extension is relevant for this context, and 0 otherwise @@ -907,7 +963,6 @@ int tls_collect_extensions(SSL_CONNECTION *s, PACKET *packet, SSLfatal(s, SSL_AD_ILLEGAL_PARAMETER, SSL_R_BAD_EXTENSION); goto err; } - /* The server must tolerate the unknown extension and complete. */ if (thisex == NULL) continue; diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c index 15ed8ef8d53..d719ef8a845 100644 --- a/ssl/statem/statem_clnt.c +++ b/ssl/statem/statem_clnt.c @@ -1799,6 +1799,8 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) if (hrr) { if (!tls_collect_extensions(s, &extpkt, SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST, &extensions, NULL, 1) + || !tls_validate_no_unknown_extensions(s, &extpkt, + SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST) || !tls_parse_extension(s, TLSEXT_IDX_ech, SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST, extensions, NULL, 0)) { @@ -1959,6 +1961,10 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) SSLfatal(s, SSL_AD_ILLEGAL_PARAMETER, SSL_R_BAD_EXTENSION); goto err; } + if (SSL_CONNECTION_IS_TLS13(s) + && !tls_validate_no_unknown_extensions(s, &extpkt, context)) + /* SSLfatal() already called */ + goto err; s->hit = 0; diff --git a/ssl/statem/statem_local.h b/ssl/statem/statem_local.h index b73c1e100f1..de9ff299be5 100644 --- a/ssl/statem/statem_local.h +++ b/ssl/statem/statem_local.h @@ -269,6 +269,8 @@ __owur int tls_validate_all_contexts(SSL_CONNECTION *s, unsigned int thisctx, RAW_EXTENSION *exts); __owur int extension_is_relevant(SSL_CONNECTION *s, unsigned int extctx, unsigned int thisctx); +__owur int tls_validate_no_unknown_extensions(SSL_CONNECTION *s, + PACKET *packet, unsigned int context); __owur int tls_collect_extensions(SSL_CONNECTION *s, PACKET *packet, unsigned int context, RAW_EXTENSION **res, size_t *len, int init); diff --git a/test/ech_corrupt_test.c b/test/ech_corrupt_test.c index 7070f4ef371..79d59844fdf 100644 --- a/test/ech_corrupt_test.c +++ b/test/ech_corrupt_test.c @@ -710,10 +710,10 @@ typedef struct { #define OSSL_ECH_BORK_GREASE (1 << 4) #define OSSL_ECH_BORK_REPLACE (1 << 5) -/* a truncated ECH, with another bogus ext to match overall length */ +/* a truncated ECH, padded with a known HRR ext to match overall length */ static unsigned char shortech[] = { 0xfe, 0x0d, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, - 0xdd, 0xdd, 0x00, 0x00 + 0x00, 0x2c, 0x00, 0x00 }; /* a too-long ECH internal length */ diff --git a/test/recipes/70-test_sslextension.t b/test/recipes/70-test_sslextension.t index 3327df474e1..89b241efee3 100644 --- a/test/recipes/70-test_sslextension.t +++ b/test/recipes/70-test_sslextension.t @@ -38,7 +38,8 @@ use constant { UNSOLICITED_SERVER_NAME => 0, UNSOLICITED_SERVER_NAME_TLS13 => 1, UNSOLICITED_SCT => 2, - NONCOMPLIANT_SUPPORTED_GROUPS => 3 + NONCOMPLIANT_SUPPORTED_GROUPS => 3, + UNKNOWN_SERVER_HELLO_TLS13 => 4 }; my $testtype; @@ -146,7 +147,8 @@ sub inject_unsolicited_extension if ($proxy->flight != 1) { if ($sent_unsolisited_extension) { my $last_record = @{$proxy->record_list}[-1]; - $fatal_alert = 1 if $last_record->is_fatal_alert(0); + my $alert = $last_record->is_fatal_alert(0); + $fatal_alert = $alert if $alert; } return; } @@ -172,6 +174,8 @@ sub inject_unsolicited_extension $type = TLSProxy::Message::EXT_SCT; } elsif ($testtype == NONCOMPLIANT_SUPPORTED_GROUPS) { $type = TLSProxy::Message::EXT_SUPPORTED_GROUPS; + } elsif ($testtype == UNKNOWN_SERVER_HELLO_TLS13) { + $type = TLSProxy::Message::EXT_UNKNOWN; } $message->set_extension($type, $ext); $message->repack(); @@ -194,7 +198,7 @@ sub inject_cryptopro_extension # Test 1-2: Sending a duplicate extension should fail. $proxy->start() or plan skip_all => "Unable to start up Proxy for tests"; -plan tests => 8; +plan tests => 9; ok($fatal_alert, "Duplicate ClientHello extension"); SKIP: { @@ -261,7 +265,7 @@ SKIP: { } SKIP: { - skip "TLS 1.3 disabled", 1 + skip "TLS 1.3 disabled", 2 if disabled("tls1_3") || (disabled("ec") && disabled("dh")); #Test 8: Inject an unsolicited extension (TLSv1.3) $fatal_alert = 0; @@ -271,4 +275,14 @@ SKIP: { $proxy->clientflags("-noservername"); $proxy->start(); ok($fatal_alert, "Unsolicited server name extension (TLSv1.3)"); + + #Test 9: Inject an unknown extension in ServerHello (TLSv1.3) + $fatal_alert = 0; + $proxy->clear(); + $proxy->filter(\&inject_unsolicited_extension); + $testtype = UNKNOWN_SERVER_HELLO_TLS13; + $proxy->clientflags(""); + $proxy->start(); + ok($fatal_alert == TLSProxy::Message::AL_DESC_UNSUPPORTED_EXTENSION, + "Unknown ServerHello extension (TLSv1.3)"); } diff --git a/util/perl/TLSProxy/Message.pm b/util/perl/TLSProxy/Message.pm index 4c1ba50de48..8c4f9564fa1 100644 --- a/util/perl/TLSProxy/Message.pm +++ b/util/perl/TLSProxy/Message.pm @@ -51,7 +51,8 @@ use constant { AL_DESC_DECRYPT_ERROR => 51, AL_DESC_PROTOCOL_VERSION => 70, AL_DESC_NO_RENEGOTIATION => 100, - AL_DESC_MISSING_EXTENSION => 109 + AL_DESC_MISSING_EXTENSION => 109, + AL_DESC_UNSUPPORTED_EXTENSION => 110 }; my %message_type = ( |
