Skip to content

fix(#756): RFC 9001 §6 key update — initiator ACK gate, consecutive detection, old-key pktnum check#824

Open
cherylsy wants to merge 10 commits into
alibaba:mainfrom
cherylsy:issue-756-key-update-initiator-confirm
Open

fix(#756): RFC 9001 §6 key update — initiator ACK gate, consecutive detection, old-key pktnum check#824
cherylsy wants to merge 10 commits into
alibaba:mainfrom
cherylsy:issue-756-key-update-initiator-confirm

Conversation

@cherylsy

@cherylsy cherylsy commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes three RFC 9001 Section 6 key update compliance bugs identified in issue #756.

BUG1 — Initiator does not gate re-initiation on ACK (§6.1)

Before: After initiating a key update, xquic immediately confirmed and could re-initiate as soon as enc_pkt_cnt exceeded the threshold, regardless of whether the peer had ACKed any new-phase packet. Violates RFC 9001 §6.1: the initiator must not start a subsequent key update until the peer has ACKed a packet protected with the current key phase.

After: key_update_initiator flag set on initiation. xqc_key_update_acked() blocks re-initiation until peer ACKs a new-phase packet. ACK processing in xqc_send_ctl_on_ack_received() clears the flag on confirmation.

BUG2 — Consecutive key update detection is dead code (§6.2)

Before: The original detection was gated on pkt_num > first_recv_pktno, but first_recv_pktno is reset to XQC_MAX_UINT64_VALUE by confirm_key_update — comparison always false, detection never fires.

First attempt (commit efec39d, reverted in beddfec): Used key_update_initiator as a pre-decrypt gate on the initiator side. This caused false positives: after the initiator flips next_in_key_phase, any old-phase packet still in flight from the peer would trigger KEY_UPDATE_ERROR, breaking key update and key update 0RTT integration tests.

After (commit d581513): Detects from the responder side instead. When the responder accepts a peer-initiated key update, it sets peer_key_update_ack_pending = TRUE and records peer_key_update_pktno. This gate is cleared only after the responder sends an APP_DATA packet containing an ACK that covers peer_key_update_pktno. If a second key-phase change arrives (with a higher packet number) before that ACK is sent, it is treated as KEY_UPDATE_ERROR. Reordered old packets with lower packet numbers are correctly allowed through.

BUG3 — Old-key high packet number not detected (§6.4)

Before: TODO comment, no enforcement.

After: Post-decrypt detection during 3xPTO old-key retention window. Guard RX key derivation with !timer_set to preserve old keys for this check.

Refactor — key_update_confirmed -> key_phase_synced, xqc_key_update_acked()

  • Rename key_update_confirmed to key_phase_synced — the old name implied ACK-based confirmation but actually tracked RX/TX key alignment (TLS-layer). key_phase_synced describes the actual semantics.
  • Extract xqc_key_update_acked() — replaces raw largest_acked >= first_sent_pktno at trigger, ACK confirmation, and test sites.

Prerequisite — TRA_KEY_UPDATE_ERROR error code

RFC 9000 assigns 0x0E to KEY_UPDATE_ERROR. xquic used 0x0E for TRA_0RTT_TRANS_PARAMS_ERROR (library-internal). Moved TRA_0RTT_TRANS_PARAMS_ERROR to 0x54, added TRA_KEY_UPDATE_ERROR = 0x0E.

CI fix — FEC test race condition

Fix clear_log / killall ordering race in FEC stream tests (XOR, RSC, PM) and frame_type_bit tests. Previous server could write [error] to slog between log truncation and process termination. Reorder to killall + sleep + clear_log.

Before / After

# RFC Before After
BUG1 §6.1 Re-initiates without ACK gate Blocked by xqc_key_update_acked() + key_update_initiator
BUG2 §6.2 pkt_num > UINT64_MAX always false Responder-side peer_key_update_ack_pending gate
BUG3 §6.4 TODO comment, no enforcement Post-decrypt check + RX derivation guard

Files Changed

File Change
include/xquic/xqc_errno.h Add TRA_KEY_UPDATE_ERROR = 0x0E, move TRA_0RTT_TRANS_PARAMS_ERROR to 0x54
src/transport/xqc_conn.h Add key_update_initiator, peer_key_update_ack_pending, peer_key_update_pktno fields; add xqc_key_update_acked(), xqc_key_update_peer_consecutive(), xqc_key_update_peer_ack_sent()
src/transport/xqc_conn.c Initialize new fields; add xqc_conn_maybe_confirm_peer_key_update_ack_sent() hook on both send paths
src/tls/xqc_tls.h Rename is_key_update_confirmed -> is_key_phase_synced
src/tls/xqc_tls.c Rename field, why-comments on state transitions
src/transport/xqc_packet_parser.c BUG2 responder-side consecutive detection + BUG3 post-decrypt detection + RX derivation guard (!timer_set) + xqc_key_update_acked() in trigger
src/transport/xqc_send_ctl.c ACK-based key_update_initiator confirmation (BUG1)
scripts/case_test.sh Update grep patterns for error code value change; fix FEC test race (XOR/RSC/PM + frame_type_bit)
tests/unittest/xqc_send_ctl_test.{c,h} 3 regression tests: BUG1 (3 cases), BUG2 (5 assertions), BUG3 (5 cases)
tests/unittest/main.c Register new test suites

Test Plan

  • run_tests — unit tests pass, 0 failures
  • xqc_test_key_update_initiator_confirmation — 3 cases: ACK below threshold, ACK at threshold, idempotency
  • xqc_test_consecutive_key_update_detection — 5 assertions: consecutive detected, reorder-low allowed, current-phase allowed, partial-ACK still gated, full-ACK clears gate
  • xqc_test_old_key_high_pktnum_detection — 5 cases: all-conditions-met, low-pktnum, timer-not-set, same-phase, mid-update
  • FEC stream tests (XOR/RSC/PM) — race condition fixed

🤖 Generated with [Claude Code]

@cherylsy
cherylsy force-pushed the issue-756-key-update-initiator-confirm branch 2 times, most recently from 3efefdc to b5e8b14 Compare June 22, 2026 11:43
@cherylsy
cherylsy force-pushed the issue-756-key-update-initiator-confirm branch from b5e8b14 to 725b50c Compare June 23, 2026 13:09
@cherylsy cherylsy changed the title fix: key update initiator must wait for ACK before confirming (RFC 9001 §6.1) fix: RFC 9001 §6 key update compliance — initiator confirmation, consecutive detection, old-key pktnum check Jun 24, 2026
@cherylsy cherylsy changed the title fix: RFC 9001 §6 key update compliance — initiator confirmation, consecutive detection, old-key pktnum check fix(#756): RFC 9001 §6 key update — initiator ACK gate, consecutive detection, old-key pktnum check Jun 25, 2026
@cherylsy
cherylsy force-pushed the issue-756-key-update-initiator-confirm branch from 604bb09 to 9dd8034 Compare June 26, 2026 06:48
cherylsy and others added 9 commits June 26, 2026 16:29
- Swap TRA_VERSION_NEGOTIATION_ERROR (0x53) before TRA_0RTT_TRANS_PARAMS_ERROR (0x54)
  so enum values are strictly ascending
- Update case_test.sh: "0RTT max_datagram_frame_size is invalid" grep from
  err:0xe (old value) to err:0x54 (new TRA_0RTT_TRANS_PARAMS_ERROR value)

Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
…learing logs

The frame_type_bit_repair_received test can fail when the previous
test's server writes [error] logs to slog between clear_log (which
truncates slog) and killall (which terminates the server). Move
killall + sleep before clear_log so the old server is fully dead
before logs are truncated.

Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
…emove redundant flag

The TLS-layer flag `key_update_confirmed` was semantically misleading: it
tracked whether RX and TX 1-RTT keys are on the same key phase (TRUE in
steady state), NOT whether the peer has ACK-confirmed a key update per
RFC 9001 §6.1.  Rename to `key_phase_synced` to match actual semantics.

Remove `key_update_not_confirmed` from xqc_key_update_ctx_t: it was always
set/cleared in lockstep with `key_update_initiator`, making it fully
redundant.  All call sites now use `key_update_initiator` directly.

No behavioral change — verified via 5-scenario trace and all unit tests pass.

Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
…dd old-key pktnum detection

BUG2 (§6.2): Remove pkt_num > first_recv_pktno from the consecutive key
update check — after confirm_key_update() resets first_recv_pktno to
XQC_MAX_UINT64_VALUE the comparison is always false, making the detection
dead code.  key_update_initiator alone is sufficient.

BUG3 (§6.4): Add post-decrypt detection for old-key-phase packets whose
pkt_num exceeds first_recv_pktno during the 3*PTO retention window.
Guard RX key derivation with !timer_set to preserve old keys for this check.

Add why-comments at key state transition sites (key_phase_synced,
first_recv_pktno sentinel, RX derivation guards, minimum pktnum tracking).

Add unit test xqc_test_old_key_high_pktnum_detection covering 5 cases.

Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
…comparison

The condition `largest_acked != MAX && largest_acked >= first_sent_pktno`
appeared raw in the key update trigger, ACK confirmation, and test code.
Extract it into xqc_key_update_acked() in xqc_conn.h so the intent
("has the peer ACKed a new-phase packet?") is explicit at every call site.

Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
…K guard

- Compress all issue-introduced comments to concise why-only form (-71 lines)
- Revert XQC_CONN_FLAG_0RTT_OK guard in xqc_conn_tls_transport_params_cb
  (0-RTT timing fix unrelated to key update, will be a separate PR)
- Revert corresponding xqc_conn_test.c changes (0RTT_OK flag, field value)

Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
The pre-decrypt detection (key_phase != next_in_key_phase &&
key_update_initiator) causes false positives on the initiator side:
after flipping next_in_key_phase, any old-phase packet still in flight
from peer will trigger KEY_UPDATE_ERROR.  This breaks "key update" and
"key update 0RTT" integration tests.

RFC 9001 §6.2 consecutive detection is MAY (not MUST) and is a
responder-side concern.  Pre-decrypt checks cannot distinguish
legitimate old packets from actual violations—only post-decrypt §6.4
detection (pkt_num > first_recv_pktno during retention window) can
reliably do so, and that check remains.

Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
…before clearing logs

Same bug pattern as 2e09e4f (frame_type_bit tests): clear_log runs
before killall, so the previous test's server can write [error] to
slog between truncation and termination.  Fix XOR, RSC, and PM stream
test blocks by reordering to killall + sleep + clear_log.

Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
@cherylsy
cherylsy force-pushed the issue-756-key-update-initiator-confirm branch from 9dd8034 to aa0ff24 Compare June 26, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant