Skip to content

Server-specific Appendix D.2 mismatch confirmed on second ClientHello after HelloRetryRequest #11083

Description

@LiD0209

Server-specific Appendix D.2 mismatch confirmed on second ClientHello after HelloRetryRequest

Scope

This document is intentionally narrow.

It keeps the current report file for the server-only Appendix D.2 requirement and does not restate the broader all-path TLSPlaintext.legacy_record_version analysis already covered elsewhere. The only question answered here is:

  • when a TLS 1.3 server sends HelloRetryRequest and then receives the second ClientHello,
  • does it still ignore the outer TLSPlaintext.legacy_record_version if only that outer record-header value changes across TLS 1.x values?

The answer is no. The current wolfSSL server still rejects that second ClientHello with VERSION_ERROR when the outer record version is changed from 0x0303 to 0x0301 or 0x0302.

Merge note:

  • this retained report subsumes the former 0021-tlsplaintext-legacy-record-version-not-ignored.md;
  • after recheck, the confirmed portion of former 0021 reduced to the same server-side HelloRetryRequest follow-up path documented here;
  • former 0021 used requirement req-f693ed8d9b17fa11b258, but the surviving confirmed behavior is the same implementation defect described by this server-specific write-up.

Standard Requirement

  • Official standard: RFC 8446
  • Primary text: RFC 8446 Appendix D.2 Negotiating with an Older Client
  • Supporting receive-path rule: RFC 8446 Section 5.1 Record Layer
  • Merged standard context from former 0021: former requirement req-f693ed8d9b17fa11b258 tracked the broader TLSPlaintext.legacy_record_version receive rule from Section 5.1 and Appendix D

Relevant Section 5.1 text:

legacy_record_version:  MUST be set to 0x0303 for all records
generated by a TLS 1.3 implementation other than an initial
ClientHello ... This field is deprecated and MUST be ignored for all purposes.

Relevant Appendix D text:

Prior versions of TLS used the record layer version number
(TLSPlaintext.legacy_record_version and
TLSCiphertext.legacy_record_version) for various purposes.  As of
TLS 1.3, this field is deprecated.  The value of
TLSPlaintext.legacy_record_version MUST be ignored by all
implementations.

Relevant Appendix D.2 text:

Note that earlier versions of TLS did not clearly specify the record
layer version number value in all cases
(TLSPlaintext.legacy_record_version).  Servers will receive various
TLS 1.x versions in this field, but its value MUST always be ignored.

Interpretation used in this report:

  • this is a server-side receive requirement;
  • the server may still validate handshake-layer version fields where RFC 8446 gives them semantics;
  • the outer unencrypted TLSPlaintext.legacy_record_version must not, by itself, flip the server from accepting to rejecting the second ClientHello.

This merged interpretation preserves the useful standard split that former 0021 made explicit:

  • handshake-layer version fields such as ClientHello.legacy_version, ServerHello.legacy_version, and HelloRetryRequest fields may still be validated where RFC 8446 says they matter;
  • the outer unencrypted record-header TLSPlaintext.legacy_record_version is different and must not be used as a receive-side validity gate.

Important scope note:

  • RFC 8446 Section 5.1 also says a sender-generated second ClientHello MUST carry outer record version 0x0303;
  • that sending-side rule does not authorize the receiver to use the outer field as a reject gate, because the same section also says the field is deprecated and MUST be ignored for all purposes.

Relevant Source Code

The decisive parser check is still in src/internal.c, but the server-only trigger in this report depends on the TLS 1.3 state transition after HelloRetryRequest.

src/internal.c:12724-12770

/* catch version mismatch */
#ifndef WOLFSSL_TLS13
    if (rh->pvMajor != ssl->version.major || rh->pvMinor != ssl->version.minor)
#else
    if (rh->pvMajor != ssl->version.major ||
        (rh->pvMinor != ssl->version.minor &&
         (!IsAtLeastTLSv1_3(ssl->version) || rh->pvMinor != tls12minor)
        ))
#endif
    {
        if (ssl->options.side == WOLFSSL_SERVER_END &&
            ssl->options.acceptState < ACCEPT_FIRST_REPLY_DONE)

            WOLFSSL_MSG("Client attempting to connect with different version");
        ...
        else {
            WOLFSSL_MSG("SSL version error");
            WOLFSSL_ERROR_VERBOSE(VERSION_ERROR);
            return VERSION_ERROR;
        }
    }

This means:

  1. the outer record header is parsed before the handshake payload is judged on its own merits;
  2. the first server-side ClientHello gets a softening branch while acceptState < ACCEPT_FIRST_REPLY_DONE;
  3. once that softening no longer applies, the same outer record-version mismatch becomes a hard VERSION_ERROR.

wolfssl/internal.h:5032-5058

The state enums put ACCEPT_FIRST_REPLY_DONE and TLS13_ACCEPT_FIRST_REPLY_DONE at the same threshold position in their respective server accept-state progressions.

src/tls13.c:15879-15889

ssl->options.acceptState = TLS13_ACCEPT_FIRST_REPLY_DONE;
WOLFSSL_MSG("accept state ACCEPT_FIRST_REPLY_DONE");
...
if (ssl->options.serverState ==
                              SERVER_HELLO_RETRY_REQUEST_COMPLETE) {
    ssl->options.clientState = CLIENT_HELLO_RETRY;
    while (ssl->options.clientState < CLIENT_HELLO_COMPLETE) {
        if ((ssl->error = ProcessReply(ssl)) < 0) {

So after the server sends HelloRetryRequest, it re-enters ProcessReply() to read the second ClientHello while already at the post-softening threshold. On this path, the outer record-version check becomes a real reject condition.

Runtime Evidence

Focused rerun date: 2026-08-03

This report now uses fresh local runtime evidence rather than the old source-only family wrapper summary.

Harness shape

  • Server: build-audit/examples/server/Debug/server.exe -v 4 -p 11111
  • Client: build-audit/examples/client/Debug/client.exe -v 4 -J -h 127.0.0.1 -p 11112
  • Proxy behavior: intercept the second client handshake record, rewrite only its two outer record-version bytes, and forward the original handshake payload unchanged

Why -J matters:

  • it forces the TLS 1.3 HelloRetryRequest path;
  • that yields two client-side plaintext handshake records;
  • the proxy can then rewrite only handshake record number 2, which is the second ClientHello.

The proxy is intentionally minimal: it rewrites only the 5-byte outer record header of the selected client handshake record and forwards the original payload bytes unchanged.

Observed results

Case Outer record version on second ClientHello Result
baseline 0x0303 Success
mutated 0x0301 Server rejected with record layer version error
mutated 0x0302 Server rejected with record layer version error

Key observations from the three runs:

  • baseline:
    • The proxy observed two client handshake records, preserved record 2 as 1603030738, and the handshake completed with the server response I hear you fa shizzle!.
  • mutated 0x0301:
    • The proxy rewrote record 2 from 1603030738 to 1603010738.
    • The server reported SSL_accept error -326, record layer version error.
  • mutated 0x0302:
    • The proxy rewrote record 2 from 1603030738 to 1603020738.
    • The server again reported SSL_accept error -326, record layer version error.

Why this runtime evidence is decisive:

  • the same server binary and the same client binary were used in all three runs;
  • the same HelloRetryRequest path was exercised in all three runs;
  • the proxy left handshake record 1 unchanged;
  • for the failing runs, only the outer record-header version bytes of handshake record 2 were changed;
  • that single outer-header change flipped the result from successful handshake to server-side VERSION_ERROR.

Inconsistency Reason

Appendix D.2 says that a server receiving TLS 1.x values in TLSPlaintext.legacy_record_version MUST always ignore that field. The current wolfSSL server does not satisfy that requirement on the second-ClientHello-after-HelloRetryRequest path, because changing only the outer record-header version from 0x0303 to 0x0301 or 0x0302 makes the server reject the record with VERSION_ERROR.

Decision Reason

This server-specific report is real.

The combination of:

  • the Appendix D.2 server-side ignore rule,
  • the post-HelloRetryRequest state transition in src/tls13.c,
  • the version-gating logic in src/internal.c,
  • and the fresh three-way differential rerun described above

shows that wolfSSL still uses the outer TLSPlaintext.legacy_record_version as a reject condition on this narrowed server receive path.

Fix Direction

The fix for this server-specific report should ensure that, after HelloRetryRequest, the server does not reject the second TLS 1.3 ClientHello solely because the outer TLSPlaintext.legacy_record_version is 0x0301 or 0x0302.

The handshake-layer validation logic can stay intact; the receive-side outer record-version field should stop acting as a gate on this path.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions