Skip to content

[Bug]: RFC 8446 violations: wolfSSL sends the wrong alert type and two alerts when an HRR is missing the supported_versions extension #10746

Description

@aeyno

Version

5.9.1

Description

When a wolfSSL TLS 1.3 client receives a HelloRetryRequest (HRR) that is missing the
mandatory supported_versions extension, it commits two distinct RFC violations:

  • Bug A — double alert: wolfSSL sends two fatal protocol_version alerts for the
    same error, violating the requirement that a fatal alert terminates the connection
    immediately.

  • Bug B — wrong alert description: wolfSSL sends a protocol_version alert (code 70)
    instead of the missing_extension alert (code 109) required by RFC 8446.

Impact

Two RFC violations. Neither poses a direct security risk.

RFC 8446 violations

Bug A — double alert

RFC 8446 §6.2 states:

"Whenever an implementation encounters a fatal error condition, it
SHOULD send an appropriate fatal alert and MUST close the connection
without sending or receiving any additional data."

After the first fatal alert the connection must be closed. wolfSSL instead sends a second
fatal alert.

Bug B — wrong alert description

RFC 8446 §4.1.4 states:

"Upon receipt of a HelloRetryRequest, the client MUST check the
legacy_version, legacy_session_id_echo, cipher_suite, and
legacy_compression_method as specified in Section 4.1.3 and then
process the extensions, starting with determining the version using
"supported_versions"."

and §6.2:

_"missing_extension: Sent by endpoints that receive a handshake
message not containing an extension that is mandatory to send for
the offered TLS version or other negotiated parameters."

When wolfSSL's client receives an HRR without the supported_versions extension, it
sends protocol_version (70) instead of missing_extension (109).

Reproduction steps

Craft a HelloRetryRequest with only a key_share extension requesting secp384r1
and no supported_versions extension:

  • Record Layer:
    • ContentType: Handshake (22)
    • Version: TLS 1.2 (legacy marker, 0x0303)
  • Handshake:
    • Type: ServerHello (2)
    • Version: 0x0303
    • Random: cf21ad74e59a6111be1d8c021e65b891c2a211167abb8c5e079e09e2c8a8339c
      (HelloRetryRequest magic value)
    • SessionID: echoed from the ClientHello
    • CipherSuite: TLS_AES_128_GCM_SHA256 (0x1301)
    • Compression: null
    • Extensions:
      • key_share requesting secp384r1 (0x0018)
      • (no supported_versions extension)

Start the following Python TCP server, which parses the session ID from the client's
ClientHello and echoes it in the malformed HRR:

import socket
import struct

HOST = "0.0.0.0"
PORT = 3000

HRR_MAGIC = bytes.fromhex(
    "CF21AD74E59A6111BE1D8C021E65B891C2A211167ABB8C5E079E09E2C8A8339C"
)
ALERT_DESC = {
    0: "close_notify", 10: "unexpected_message", 40: "handshake_failure",
    47: "illegal_parameter", 70: "protocol_version", 109: "missing_extension",
    100: "no_renegotiation",
}


def extract_session_id(ch: bytes) -> bytes:
    """Return the session_id bytes from a raw TLS ClientHello record."""
    # Record (5) + Handshake header (4) + legacy_version (2) + random (32) = 43
    off = 43
    if len(ch) <= off:
        return b""
    sid_len = ch[off]
    return ch[off + 1 : off + 1 + sid_len]


def make_hrr(session_id: bytes) -> bytes:
    """
    Build a HelloRetryRequest with only a key_share extension (secp384r1).
    The required supported_versions extension is intentionally omitted.
    RFC 8446 §4.2.1: the client MUST abort with a 'missing_extension' alert.
    """
    body  = b"\x03\x03"                           # legacy_version
    body += HRR_MAGIC                              # HRR random
    body += bytes([len(session_id)]) + session_id  # session_id echo
    body += b"\x13\x01"                            # TLS_AES_128_GCM_SHA256
    body += b"\x00"                                # compression = null
    # Extensions: key_share only (type 0x0033, len 2, group secp384r1 = 0x0018)
    exts  = b"\x00\x33\x00\x02\x00\x18"
    body += struct.pack(">H", len(exts)) + exts
    hs = b"\x02" + struct.pack(">I", len(body))[1:] + body
    return b"\x16\x03\x03" + struct.pack(">H", len(hs)) + hs


def parse_alerts(data: bytes) -> list[str]:
    msgs = []
    i = 0
    while i + 5 <= len(data):
        rec_t = data[i]
        rec_l = struct.unpack(">H", data[i + 3 : i + 5])[0]
        body  = data[i + 5 : i + 5 + rec_l]
        i    += 5 + rec_l
        if rec_t == 0x15 and len(body) >= 2:
            level = "fatal" if body[0] == 2 else "warning"
            desc  = ALERT_DESC.get(body[1], body[1])
            msgs.append(f"Alert({level},{desc})")
    return msgs


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((HOST, PORT))
    srv.listen(1)
    print(f"[*] Listening on {HOST}:{PORT} ...")

    conn, addr = srv.accept()
    with conn:
        print(f"[+] Connection from {addr}")
        ch = conn.recv(4096)
        print(f"[>] Received ClientHello ({len(ch)} bytes)")

        sid = extract_session_id(ch)
        hrr = make_hrr(sid)
        conn.sendall(hrr)
        print(f"[<] Sent malformed HRR (no supported_versions): {hrr.hex()}")

        conn.settimeout(3)
        data = b""
        try:
            while True:
                chunk = conn.recv(4096)
                if not chunk:
                    break
                data += chunk
        except socket.timeout:
            pass

        alerts = parse_alerts(data)
        result = ", ".join(alerts) if alerts else f"raw: {data[:20].hex()}"
        print(f"[>] Client response: {result}")

Then initiate a TLS 1.3 handshake with a wolfSSL client:

./build/examples/client/client -p 3000 -v 4

Expected behavior (RFC 8446): the client sends exactly one missing_extension
fatal alert and closes the connection.

Acknowledgements

This bug was found thanks to the tlspuffin fuzzer designed and developed by the tlspuffin team:

  • Max Ammann
  • Olivier Demengeon - Loria, Inria
  • Tom Gouville - Loria, Inria
  • Lucca Hirschi - Loria, Inria
  • Steve Kremer - Loria, Inria
  • Michael Mera - Loria, Inria

Metadata

Metadata

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions