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:
- the outer record header is parsed before the handshake payload is judged on its own merits;
- the first server-side
ClientHello gets a softening branch while acceptState < ACCEPT_FIRST_REPLY_DONE;
- 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.
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_versionanalysis already covered elsewhere. The only question answered here is:HelloRetryRequestand then receives the secondClientHello,TLSPlaintext.legacy_record_versionif only that outer record-header value changes across TLS 1.x values?The answer is no. The current wolfSSL server still rejects that second
ClientHellowithVERSION_ERRORwhen the outer record version is changed from0x0303to0x0301or0x0302.Merge note:
0021-tlsplaintext-legacy-record-version-not-ignored.md;0021reduced to the same server-sideHelloRetryRequestfollow-up path documented here;0021used requirementreq-f693ed8d9b17fa11b258, but the surviving confirmed behavior is the same implementation defect described by this server-specific write-up.Standard Requirement
Negotiating with an Older ClientRecord Layer0021: former requirementreq-f693ed8d9b17fa11b258tracked the broaderTLSPlaintext.legacy_record_versionreceive rule from Section 5.1 and Appendix DRelevant Section 5.1 text:
Relevant Appendix D text:
Relevant Appendix D.2 text:
Interpretation used in this report:
TLSPlaintext.legacy_record_versionmust not, by itself, flip the server from accepting to rejecting the secondClientHello.This merged interpretation preserves the useful standard split that former
0021made explicit:ClientHello.legacy_version,ServerHello.legacy_version, andHelloRetryRequestfields may still be validated where RFC 8446 says they matter;TLSPlaintext.legacy_record_versionis different and must not be used as a receive-side validity gate.Important scope note:
ClientHelloMUST carry outer record version0x0303;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 afterHelloRetryRequest.src/internal.c:12724-12770This means:
ClientHellogets a softening branch whileacceptState < ACCEPT_FIRST_REPLY_DONE;VERSION_ERROR.wolfssl/internal.h:5032-5058The state enums put
ACCEPT_FIRST_REPLY_DONEandTLS13_ACCEPT_FIRST_REPLY_DONEat the same threshold position in their respective server accept-state progressions.src/tls13.c:15879-15889So after the server sends
HelloRetryRequest, it re-entersProcessReply()to read the secondClientHellowhile 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-03This report now uses fresh local runtime evidence rather than the old source-only family wrapper summary.
Harness shape
build-audit/examples/server/Debug/server.exe -v 4 -p 11111build-audit/examples/client/Debug/client.exe -v 4 -J -h 127.0.0.1 -p 11112Why
-Jmatters:HelloRetryRequestpath;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
ClientHello0x03030x0301record layer version error0x0302record layer version errorKey observations from the three runs:
1603030738, and the handshake completed with the server responseI hear you fa shizzle!.0x0301:1603030738to1603010738.SSL_accept error -326, record layer version error.0x0302:1603030738to1603020738.SSL_accept error -326, record layer version error.Why this runtime evidence is decisive:
HelloRetryRequestpath was exercised in all three runs;VERSION_ERROR.Inconsistency Reason
Appendix D.2 says that a server receiving TLS 1.x values in
TLSPlaintext.legacy_record_versionMUST always ignore that field. The current wolfSSL server does not satisfy that requirement on the second-ClientHello-after-HelloRetryRequestpath, because changing only the outer record-header version from0x0303to0x0301or0x0302makes the server reject the record withVERSION_ERROR.Decision Reason
This server-specific report is real.
The combination of:
HelloRetryRequeststate transition insrc/tls13.c,src/internal.c,shows that wolfSSL still uses the outer
TLSPlaintext.legacy_record_versionas 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.3ClientHellosolely because the outerTLSPlaintext.legacy_record_versionis0x0301or0x0302.The handshake-layer validation logic can stay intact; the receive-side outer record-version field should stop acting as a gate on this path.