Skip to content

fix(signing): structured-headers parser + bucketed replay store (#581, #582)#596

Merged
bokelley merged 1 commit into
mainfrom
bokelley/issues-581-582
Apr 19, 2026
Merged

fix(signing): structured-headers parser + bucketed replay store (#581, #582)#596
bokelley merged 1 commit into
mainfrom
bokelley/issues-581-582

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Closes #581 (parser swap). Closes #582 (replay store).

#581 — swap to structured-headers

Hand-rolled Signature-Input / Signature / Content-Digest parsers replaced with the maintained structured-headers library (RFC 8941 / RFC 9651).

  • ~90 lines of bespoke state-machine code removed (src/lib/signing/parser.ts + content-digest.ts).
  • AdCP-profile checks kept as thin typed wrappers: required-param presence, tag match, alg allowlist, quoted-string typing for nonce/keyid/alg/tag, integer typing for created/expires.
  • Signature byte-sequence values stay base64url-tolerant — we normalise -/_+// only inside :…: delimiters before handing to the strict SF parser.
  • Content-Digest keeps a regex fallback so a malformed filler member (e.g. truncated sha-512) does not mask the sha-256 entry we actually verify against.
  • signatureParamsValue is now serializeInnerList(entry) instead of raw substring splicing — verified byte-identical on the reordered-params review-hardening vector.
  • Escape decoding now follows RFC 8941 §3.3.3 (the old parser stored the literal \" escape sequence, the library decodes to "). The one escape-handling test assertion updated to match correct behaviour.

#582 — time-bucketed replay store

InMemoryReplayStore now groups entries by floor(expiresAt / bucketSizeSeconds) (default 60s). Whole buckets evict in one step when their latest expiry has passed, eliminating the per-call O(N) filter sweep that turned a near-cap keyid into a quadratic DoS target.

  • has() / insert() / isCapHit() are O(1) amortized per keyid regardless of entries-per-keyid.
  • Prune runs at most once per bucket tick per keyid.
  • Default maxEntriesPerKeyid: 1,000,000 → 100,000 (still ample; new InMemoryReplayStore({ maxEntriesPerKeyid }) overrides).
  • ReplayStore interface unchanged.

Test plan

  • All 28 RFC 9421 conformance vectors pass
  • test/request-signing-review-fixes.test.js passes (with 2 assertion tweaks for the correct RFC 8941 escape-decoding + library-surfaced parse-error messages; intent preserved, + a new created=1.5 integer-rejection assertion)
  • test/request-signing-e2e.test.js passes
  • New test/request-signing-replay-perf.test.js asserts sub-linear (< 4×) growth of has() / insert() latency when entries-per-keyid grows 50×, plus a bucket-eviction correctness check
  • Full npm test green (3601 passing)
  • Changesets added (two patch entries)

🤖 Generated with Claude Code

…#582)

#581: swap the hand-rolled Signature-Input / Signature / Content-Digest
parsers for the maintained `structured-headers` library. AdCP-profile
checks (required params, tag match, alg allowlist, quoted-string typing
for nonce/keyid/alg/tag, integer typing for created/expires) stay in
this package as thin typed wrappers. Signature byte-sequence values
remain base64url-tolerant (normalised before handing to the strict
parser); Content-Digest keeps its regex fallback so a malformed filler
member (truncated sha-512) does not mask the sha-256 entry we verify
against. ~90 lines of bespoke state-machine code removed.

#582: time-bucket the in-memory replay store so has() / insert() /
isCapHit() stay O(1) amortized on a hot keyid pinned near the per-keyid
cap. Entries group by floor(expiresAt / bucketSizeSeconds) (default
60s); whole buckets evict in one step when their latest expiry has
passed, eliminating the per-call O(N) filter sweep that turned a
near-cap keyid into a quadratic DoS target. Default maxEntriesPerKeyid
drops 1,000,000 → 100,000 (still ample; configurable). ReplayStore
interface unchanged.

All 28 conformance vectors + review-hardening suite pass. Perf suite
in test/request-signing-replay-perf.test.js asserts sub-linear growth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 18cefcc into main Apr 19, 2026
12 checks passed
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.

signing: time-bucketed replay store to avoid O(N) prune on hot keyids signing: swap hand-rolled parser for a Structured Fields library (RFC 8941)

1 participant