migration(rs256): phase 121 — SDK TokenVerifier accepts both RS256 + HS256#23
Merged
khaliqgant merged 2 commits intomainfrom Apr 23, 2026
Merged
Conversation
Adds HS256 as an accepted verification algorithm alongside the
existing RS256/EdDSA paths, gated on RELAYAUTH_VERIFIER_ACCEPT_HS256.
Default is accept-HS256-during-migration; ops flips to "false" after
the cutover soak in phase 122.
Implementation:
- resolveVerificationAlgorithm adds HS256 → { name: "HMAC", hash:
"SHA-256" } when the env flag is not "false".
- importVerificationKey for HS256 reads the symmetric key bytes from
the JWK's "k" field (RFC 7518) and imports as an HMAC verify-only
key. RSA/EdDSA paths unchanged.
- Dispatch picks "HMAC" for HS256 tokens and the original RSA/EdDSA
AlgorithmIdentifier for those paths; kty check in matchesJwk stays
the source of truth for algorithm/key-type pairing, defeating
alg-confusion.
Peer review: two independent reviewers evaluated this exact diff
via agent-relay on 2026-04-22 and both approved:
- crypto-reviewer verdict: "All eight tests pass, typecheck is
clean, and I walked every attack class through the code to
confirm the reject path. The verifier is sound for dual-
acceptance during the HS256 → RS256 migration. No changes
required to merge; the four advisory items can be handled as
follow-ups."
- compat-reviewer verdict: "No breaking changes, no consumer
updates required. Drop-in compatible, zero code changes
required in sage."
(The workflow's synthesize step idled out — a runner heuristic
issue unrelated to the code. Committing by hand since the peer
reviews themselves ran to completion and produced explicit
approvals.)
Adversarial tests in verify-dual-alg.test.ts cover:
- RS256 happy path (regression)
- HS256 happy path with k-field JWK
- Alg confusion: HS256 token against RSA-only JWKS rejected
- alg=none rejected
- Payload tampering invalidates signature
- Missing kid rejected
- Kid not in JWKS rejected
- Sunset flag: RELAYAUTH_VERIFIER_ACCEPT_HS256=false rejects HS256
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…emantics
assertPhase0LegacyHs256Algorithm used to assume the SDK verifier would
reject any HS256 token as an unsupported algorithm. With dual-verify
(this PR), HS256 is accepted by default, so the old assertion now sees
'jwks_fetch_failed' (the verifier looks for a matching HS256 JWK that
the test harness doesn't publish) instead of 'invalid_token'.
Fix: set RELAYAUTH_VERIFIER_ACCEPT_HS256=false around the verify call
so the test asserts the post-sunset posture that phase 122 flips in
production. Test intent ("spec-compliant verifier rejects legacy
HS256") is preserved; the fixture just scopes to the correct flag
state.
Env var is restored in a finally block to avoid cross-test leakage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
Phase 121 — SDK dual-verify (RS256 + HS256)
Updates
@relayauth/sdk'sTokenVerifierto accept both RS256 (new) and HS256 (legacy) during the cutover window. Without this, once phase 122 flips the signer to RS256, any verifier still on the old SDK fails closed.Part of the api-keys + RS256 migration. See
specs/api-keys-and-rs256-migration.mdfor the full design. This is phase 3a, depends on phase 120 (RS256 signing path), feeds phase 122 (production cutover).What changed
packages/sdk/typescript/src/verify.ts: HS256 added toresolveVerificationAlgorithm(mapped to{ name: "HMAC", hash: "SHA-256" }) and gated onRELAYAUTH_VERIFIER_ACCEPT_HS256.importVerificationKeylearns an HS256 branch that reads the symmetric key from the JWK'skfield per RFC 7518 and imports as an HMAC verify-only key. Thektygate inmatchesJwkremains the authoritative algorithm/key-type pairing check.packages/sdk/typescript/src/__tests__/verify-dual-alg.test.ts: 8 adversarial tests covering happy paths, alg confusion,alg=none, payload tampering, kid spoofing, and the sunset-flag behavior.Peer review (ran via agent-relay, explicit verdicts)
Two independent reviewers evaluated this exact diff on 2026-04-22. Both approved.
crypto-reviewer:
Advisory follow-ups (non-blocking, track separately):
RELAYAUTH_VERIFIER_ACCEPT_HS256polarity so missing config rejects HS256 (fail-closed posture).algmatching the request (currently JWKs withoutalgstill pass whenktymatches).kty === "oct"re-check at HS256 key import (relies on upstreamselectJwkfilter today).{ kty: "RSA", alg: "HS256", k: "…" }— blocked today viaktygate, but a future refactor ofmatchesJwkcould silently open it).compat-reviewer:
Two behavior changes documented (not breaking):
RELAYAUTH_VERIFIER_ACCEPT_HS256=false.matchesJwkalg matching is stricter: a JWK missingalgno longer matches requests with a specific alg. No-op for well-formed providers; external JWKS providers that omitalgmay see newinvalid_tokenerrors.Verification
node --test --import tsx packages/sdk/typescript/src/__tests__/verify-dual-alg.test.ts: 8/8 passnpx turbo typecheck --filter=@relayauth/sdk: cleanNote on workflow origin
The agent-relay workflow for this phase ran through the review gates successfully but then failed in the
synthesizestep (a runner heuristic: the architect agent idled for 30s and was marked incomplete before emitting the expected completion token). The reviews themselves produced complete outputs with explicit verdicts, so I committed the reviewed diff by hand and pasted the verdicts above as evidence. Thesynthesizefailure is a workflow-framework issue unrelated to this code and will be tracked separately.Run order in the migration
This PR is phase 121. 118/119/120 are already merged. After this merges, bump package versions, publish
@relayauth/serverand@relayauth/sdk, then bump../clouddeps before phase 122.