Skip to content

Confirmed Issue in Ignoring Repeated Handshake change_cipher_spec Records #11084

Description

@LiD0209

Confirmed Issue in Ignoring Repeated Handshake change_cipher_spec Records

Problem Description

The original report stopped at suspected_issue because the earlier runtime wrapper only showed that the generic TLS 1.3 positive control passed and did not isolate the exact CCS replay behavior. A focused recheck now shows the real boundary:

  • a single extra legal plaintext CCS in the RFC window is ignored and the handshake succeeds;
  • a second consecutive legal plaintext CCS, still in the same RFC-allowed window, causes wolfSSL to abort the handshake.

That means wolfSSL does not ignore every legal handshake-phase CCS as RFC 8446 requires. The baseline single-CCS path acts as a positive control. The confirmed defect is the replay or repeated-record path.

Standard Requirement

Primary normative text:

An implementation may receive an unencrypted record of type
change_cipher_spec consisting of the single byte value 0x01 at any
time after the first ClientHello message has been sent or received
and before the peer's Finished message has been received and MUST
simply drop it without further processing.

Supporting text:

change_cipher_spec at any time during the handshake, as they must be
ignored by the peer

Interpretation:

Within the RFC window, every legal plaintext CCS record must be dropped without further processing. The text does not impose a one-record limit per handshake gap.

Code Analysis

The key receive-side logic is in src/internal.c.

src/internal.c:23932-23968

DoChangeCipherSpecTls13() accepts the first legal plaintext CCS but tracks it with ssl->msgsReceived.got_change_cipher:

if (!ssl->msgsReceived.got_change_cipher) {
    ssl->msgsReceived.got_change_cipher = 1;
}
else {
    SendAlert(ssl, alert_fatal, illegal_parameter);
    WOLFSSL_ERROR_VERBOSE(UNKNOWN_RECORD_TYPE);
    return UNKNOWN_RECORD_TYPE;
}

This is not "drop every legal CCS". It is "accept at most one CCS until the next handshake record is processed".

src/internal.c:24908-24912

Before processing a real TLS 1.3 handshake record, wolfSSL resets the gate:

ssl->msgsReceived.got_change_cipher = 0;
ret = DoTls13HandShakeMsg(ssl,
    ssl->buffers.inputBuffer.buffer,
    &ssl->buffers.inputBuffer.idx,
    ssl->curStartIdx + ssl->curSize);

So the effective behavior is:

  1. first legal CCS before a handshake message: tolerated;
  2. second legal CCS before that handshake message: rejected;
  3. only after the handshake record is consumed is the CCS allowance reset.

src/internal.c:12797-12834

Each new record resets ssl->keys.decryptedCur to 0, so the reproduced failure is not explained by a stale "already decrypted current record" state:

/* haven't decrypted this record yet */
ssl->keys.decryptedCur = 0;

That keeps the root cause focused on the got_change_cipher one-per-gap gate.

Runtime Evidence

Existing Round-1 Evidence

The original wrapper completed a normal TLS 1.3 handshake but did not isolate repeated plaintext CCS handling. It therefore left the replay boundary unresolved and is used only as historical context.

Focused Recheck on 2026-08-03

Probe setup and action:

  • Run the audited wolfSSL TLS 1.3 server and client.
  • Insert legal plaintext CCS records (14 03 03 00 01 01) immediately after the client's first outbound handshake flight.
  • Compare one injected CCS versus two consecutive injected CCS records.

Observed results:

  • Case inject_count=1:
    • client_rc=0
    • server_rc=0
    • Handshake completes successfully.
  • Case inject_count=2:
    • client_rc=1
    • server_rc=1
    • Client output includes SSL_read reply error -313, received alert fatal error
    • Server output includes SSL_accept error -311, unknown type in record hdr

Interpretation:

The second legal CCS is still inside the RFC 8446 Section 5 "drop it without further processing" window, but wolfSSL rejects it instead of dropping it.

Decision Reason

RFC 8446 requires every legal plaintext CCS in the allowed handshake window to be ignored. wolfSSL instead enforces a narrower policy: one legal CCS per handshake gap, with the second legal CCS rejected until a real handshake record resets the flag. The focused runtime probe reproduces that exact boundary and confirms that the implementation violates the standard.

Remaining Uncertainty

None for the repeated legal plaintext CCS path. The remaining nuance is only scope:

  • the single-CCS baseline path behaves correctly;
  • the repeated-CCS replay or state-order path is the confirmed defect.

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