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
Open
Conversation
cherylsy
force-pushed
the
issue-756-key-update-initiator-confirm
branch
2 times, most recently
from
June 22, 2026 11:43
3efefdc to
b5e8b14
Compare
cherylsy
force-pushed
the
issue-756-key-update-initiator-confirm
branch
from
June 23, 2026 13:09
b5e8b14 to
725b50c
Compare
cherylsy
force-pushed
the
issue-756-key-update-initiator-confirm
branch
from
June 26, 2026 06:48
604bb09 to
9dd8034
Compare
- 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
force-pushed
the
issue-756-key-update-initiator-confirm
branch
from
June 26, 2026 08:32
9dd8034 to
aa0ff24
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_cntexceeded 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_initiatorflag set on initiation.xqc_key_update_acked()blocks re-initiation until peer ACKs a new-phase packet. ACK processing inxqc_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, butfirst_recv_pktnois reset toXQC_MAX_UINT64_VALUEbyconfirm_key_update— comparison always false, detection never fires.First attempt (commit efec39d, reverted in beddfec): Used
key_update_initiatoras a pre-decrypt gate on the initiator side. This caused false positives: after the initiator flipsnext_in_key_phase, any old-phase packet still in flight from the peer would triggerKEY_UPDATE_ERROR, breakingkey updateandkey update 0RTTintegration 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 = TRUEand recordspeer_key_update_pktno. This gate is cleared only after the responder sends an APP_DATA packet containing an ACK that coverspeer_key_update_pktno. If a second key-phase change arrives (with a higher packet number) before that ACK is sent, it is treated asKEY_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_setto preserve old keys for this check.Refactor —
key_update_confirmed->key_phase_synced,xqc_key_update_acked()key_update_confirmedtokey_phase_synced— the old name implied ACK-based confirmation but actually tracked RX/TX key alignment (TLS-layer).key_phase_synceddescribes the actual semantics.xqc_key_update_acked()— replaces rawlargest_acked >= first_sent_pktnoat trigger, ACK confirmation, and test sites.Prerequisite —
TRA_KEY_UPDATE_ERRORerror codeRFC 9000 assigns
0x0EtoKEY_UPDATE_ERROR. xquic used0x0EforTRA_0RTT_TRANS_PARAMS_ERROR(library-internal). MovedTRA_0RTT_TRANS_PARAMS_ERRORto0x54, addedTRA_KEY_UPDATE_ERROR = 0x0E.CI fix — FEC test race condition
Fix
clear_log/killallordering 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 tokillall + sleep + clear_log.Before / After
xqc_key_update_acked()+key_update_initiatorpkt_num > UINT64_MAXalways falsepeer_key_update_ack_pendinggateFiles Changed
include/xquic/xqc_errno.hTRA_KEY_UPDATE_ERROR = 0x0E, moveTRA_0RTT_TRANS_PARAMS_ERRORto0x54src/transport/xqc_conn.hkey_update_initiator,peer_key_update_ack_pending,peer_key_update_pktnofields; addxqc_key_update_acked(),xqc_key_update_peer_consecutive(),xqc_key_update_peer_ack_sent()src/transport/xqc_conn.cxqc_conn_maybe_confirm_peer_key_update_ack_sent()hook on both send pathssrc/tls/xqc_tls.his_key_update_confirmed->is_key_phase_syncedsrc/tls/xqc_tls.csrc/transport/xqc_packet_parser.c!timer_set) +xqc_key_update_acked()in triggersrc/transport/xqc_send_ctl.ckey_update_initiatorconfirmation (BUG1)scripts/case_test.shtests/unittest/xqc_send_ctl_test.{c,h}tests/unittest/main.cTest Plan
run_tests— unit tests pass, 0 failuresxqc_test_key_update_initiator_confirmation— 3 cases: ACK below threshold, ACK at threshold, idempotencyxqc_test_consecutive_key_update_detection— 5 assertions: consecutive detected, reorder-low allowed, current-phase allowed, partial-ACK still gated, full-ACK clears gatexqc_test_old_key_high_pktnum_detection— 5 cases: all-conditions-met, low-pktnum, timer-not-set, same-phase, mid-update🤖 Generated with [Claude Code]