Skip to content

test(signing): live-verifier integration for HTTPS stores#616

Merged
bokelley merged 2 commits into
mainfrom
bokelley/live-verifier-integration
Apr 19, 2026
Merged

test(signing): live-verifier integration for HTTPS stores#616
bokelley merged 2 commits into
mainfrom
bokelley/live-verifier-integration

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Closes #609. Dev-only integration test — no library surface changes, no public barrel additions, no changeset.

Why

Existing signing test coverage splits into:

  • Per-store unit tests (request-signing-https-stores.test.js): HttpsJwksResolver and HttpsRevocationStore in isolation.
  • Conformance grader (request-signing-grader-*.test.js): RFC 9421 vectors against a StaticJwksResolver + InMemoryRevocationStore.

Neither runs the real HTTPS stores behind verifyRequestSignature across a multi-step flow where the origin-side state changes mid-session. That's the wiring between PR #601's new moving parts and the verifier pipeline — pilots that depend on rotation/revocation working transparently need that integration covered.

Scope / non-scope

  • In: real HttpsJwksResolver + HttpsRevocationStore behind verifyRequestSignature, with origin-side state transitions between requests. Also createExpressVerifier so we pin middleware SSRF handling.
  • Out: spec-conformance grading (that's feat: RFC 9421 request-signing conformance grader (#585) #600). External agents have no way to let a remote tester drive their JWKS / revocation snapshots, so this file intentionally stays outside @adcp/client/testing/storyboard/request-signing — the operator-facing grader stays pure spec-conformance.

Scenarios (8 tests)

# Scenario Asserts
1 Mid-run JWKS rotation Rotated kid verifies after cooldown; request count increments on refresh
2 Mid-run revocation publish Freshly-revoked kid rejected at step 9; revocation request count increments
3 Stale-revocation fail-closed Origin silent past next_update + gracerequest_signature_revocation_stale
4 Garbage-response-during-grace Origin serves 500/HTML after next_update; within grace = cached snapshot enforced; past grace = fail-closed
5 JWKS URL at IMDS SsrfRefusedError with code always_blocked_address surfaces from verifyRequestSignature
6 Revocation URL at loopback SsrfRefusedError with code private_address surfaces
7 Middleware SSRF pass-through createExpressVerifier sends the error to next(err), NOT mapped to 401
8 Combined rotation + revocation Four sequential requests across one verifier; cross-store cache interactions

Expert-review fixes in-PR

Both code-reviewer and security-reviewer ran on the draft.

Code review Should-Fix → fixed:

  • Extract startJwksServer / startRevocationServer to test/helpers/signing-origin-servers.js instead of duplicating from the existing unit-test file.
  • Every scenario wraps server lifecycle in withServers() (guarantees stop() runs even when setup throws — avoids a CI hang masking a real regression).
  • Harmonize rejection-predicate style (all use err instanceof X && err.code === 'y').

Code review Nice-to-Have → added:

  • Garbage-response-during-grace scenario (likelier real incident than clean silence).

Security review Should-Fix → fixed:

  • Added middleware SSRF test — createExpressVerifier routes SSRF refusals to next(err), not 401. Future regression that wrapped SSRF as request_signature_key_unknown (a plausible shortcut in the JWKS resolver's catch path) would silently leak SSRF refusals as 401, train callers to retry, and shift attack surface. Pinning it down.
  • Hit-counter assertions on both rotation and revocation servers so tests can't pass coincidentally when the refresh path breaks.

Security review Consider → deferred:

  • maxValidityWindowSeconds surfaced through verifier → already covered by store unit tests, not load-bearing here.
  • Concurrent-verify race against in-flight dedup → complex to write deterministically; worth a follow-up if we see it in the wild.

Test plan

🤖 Generated with Claude Code

bokelley and others added 2 commits April 19, 2026 07:17
Dev-only CI artifact. Fills the integration-test gap between the per-store
unit tests (`request-signing-https-stores.test.js`) and the RFC 9421
conformance grader (`request-signing-grader-*.test.js`): the real HTTPS
stores run inside the verifier pipeline across multi-request flows where
the origin-side JWKS / revocation state changes mid-session.

Scope: not a specialism grader. External agents have no way to let a
remote tester drive their JWKS / revocation endpoints, so this file
intentionally lives outside `@adcp/client/testing/storyboard/request-signing`
(which stays pure spec-conformance) and is not exposed via any public
barrel.

Scenarios (8 tests):
- Mid-run JWKS rotation — verifier picks up a rotated kid after cooldown
  without a process restart.
- Mid-run revocation publish — freshly-revoked kid rejected at step 9 on
  the next verify past next_update. Hit-counter assertion pins the
  refresh actually fired (vs. passing coincidentally on replay-cache
  collision).
- Stale-revocation fail-closed — origin silent past next_update + grace
  surfaces `request_signature_revocation_stale`.
- Garbage-response-during-grace — origin serves 500/HTML after
  next_update. Within grace: cached snapshot still enforced. Past grace:
  fail-closed. The likeliest real incident (flaky CDN, wrong
  content-type) and the property that stops routine origin blips from
  cascading into cross-counterparty outages.
- JWKS URL at IMDS → `SsrfRefusedError` surfaces from verifier.
- Revocation URL at loopback without dev opt-in → `SsrfRefusedError`
  surfaces.
- Middleware SSRF pass-through — `createExpressVerifier` sees the error
  at `next(err)`, NOT mapped to a 401. Locks in the invariant that SSRF
  refusals don't get laundered as transient auth failures a misbehaving
  caller might retry.
- Combined rotation + revocation — one verifier, four sequential
  requests, exercises the cross-store cache / refresh interaction that
  can't surface when each store is tested in isolation.

Expert review in-PR:
- Extracted `startJwksServer` / `startRevocationServer` to
  `test/helpers/signing-origin-servers.js` instead of duplicating the
  existing unit-test copies. Shared helper adds a `responseOverride`
  hook needed by the garbage-response scenario.
- Every scenario now wraps server lifecycle in `withServers()` (avoids a
  CI hang if setup throws before a try block would have been entered).
- Tightened assertions: rotation and revocation tests assert request
  counts incremented, not just that the verdict matches. Loose shape
  assertions were letting regressions in adjacent paths pass by
  coincidence.
- Middleware SSRF test added in response to the security reviewer's
  observation that `createExpressVerifier` handles SSRF differently from
  `verifyRequestSignature` and the file didn't pin that down.

No library code changes. No changeset (test files only per project
guidelines).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups on PR #616:

- Changeset CI gate fails on any non-doc file change without a
  corresponding changeset, even for test-only PRs. Adding an empty
  changeset to satisfy the check.
- Security reviewer flagged the garbage-response-during-grace scenario
  for proving only that a non-revoked kid still verifies during grace —
  a regression that cleared the cached revocation list on refresh-failure
  would silently pass. Adding an `already-revoked-kid` to the JWKS and
  asserting it stays rejected at step 9 during grace closes that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit b66593a 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: live-verifier storyboard that exercises HttpsJwksResolver + HttpsRevocationStore end-to-end

1 participant