Version
wolfssl 5.9.1
Description
When a wolfSSL TLS 1.3 client receives a ServerHello with legacy_version = 0x0303 (TLS 1.2)
and no supported_versions extension, it sends a decode_error alert (code 50) and closes the
connection.
The correct alert per RFC 8446 §6.2 is protocol_version (code 70): the server offered a
version the TLS 1.3-only client does not support, which is a version-negotiation failure, not a
decode error, the message is correctly encoded. OpenSSL 3.4.0 sends protocol_version for the same input.
Impact
RFC violation.
RFC 8446 violation
RFC 8446 §6.2 defines:
"decode_error: A message could not be decoded because some field was
out of the specified range or the length of the message was
incorrect. This alert is used for errors where the message does
not conform to the formal protocol syntax."
The message is syntactically valid per the RFC so there should not be a decode error.
Reproduction steps
Craft a TLS 1.2 ServerHello with no extensions:
- Record Layer:
- ContentType: Handshake (22)
- Version: TLS 1.2 legacy marker (0x0303)
- Handshake:
- Type: ServerHello (2)
legacy_version: 0x0303
- Random: 32 arbitrary bytes (not the HRR magic constant)
- SessionID: echoed from the ClientHello
- CipherSuite:
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xC02B)
- Compression: null (0x00)
- Extensions: (absent — no extensions length field, no extensions data)
Start the following Python TCP server, which parses the session ID from the client's
ClientHello and echoes it in the crafted ServerHello:
import socket
import struct
HOST = "0.0.0.0"
PORT = 3000
ALERT_DESC = {
0: "close_notify", 10: "unexpected_message", 40: "handshake_failure",
47: "illegal_parameter", 50: "decode_error", 70: "protocol_version",
109: "missing_extension",
}
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_server_hello(session_id: bytes) -> bytes:
"""
Build a TLS 1.2 ServerHello with no extensions.
A TLS 1.3-only client MUST reject this with a protocol_version alert (RFC 8446 §6.2).
"""
rand = bytes([0x42] * 32) # arbitrary random, not HRR magic
body = b"\x03\x03" # legacy_version = TLS 1.2
body += rand
body += bytes([len(session_id)]) + session_id
body += b"\xc0\x2b" # TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
body += b"\x00" # compression = null
# No extensions field: TLS 1.2 ServerHello without extensions is valid on-the-wire.
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)
sh = make_server_hello(sid)
conn.sendall(sh)
print(f"[<] Sent TLS 1.2 ServerHello (no extensions): {sh.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-only handshake with a wolfSSL client:
./build/examples/client/client -p 3000 -v 4
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
Version
wolfssl 5.9.1
Description
When a wolfSSL TLS 1.3 client receives a ServerHello with
legacy_version = 0x0303(TLS 1.2)and no
supported_versionsextension, it sends adecode_erroralert (code 50) and closes theconnection.
The correct alert per RFC 8446 §6.2 is
protocol_version(code 70): the server offered aversion the TLS 1.3-only client does not support, which is a version-negotiation failure, not a
decode error, the message is correctly encoded. OpenSSL 3.4.0 sends
protocol_versionfor the same input.Impact
RFC violation.
RFC 8446 violation
RFC 8446 §6.2 defines:
The message is syntactically valid per the RFC so there should not be a decode error.
Reproduction steps
Craft a TLS 1.2 ServerHello with no extensions:
legacy_version:0x0303TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xC02B)Start the following Python TCP server, which parses the session ID from the client's
ClientHelloand echoes it in the crafted ServerHello:Then initiate a TLS 1.3-only handshake with a wolfSSL client:
Acknowledgements
This bug was found thanks to the tlspuffin fuzzer designed and developed by the tlspuffin team: