Repository navigation
Conversation
… E7 F4) #361 commits sessionKeyAuthDigest = sha256(canonicalize(SessionKeyAuthorization)). The cross-family review found one authorization with several digests: issueSessionKey (and deriveSessionKey) put the caller's scope.allowedActions and scope.contractIds into the emitted key verbatim, and canonicalSessionKeyBytes sorts a copy for signing but keeps duplicates. Reproduced first on 75fd440, tests only, no source change: - verifier ephemeral-identity-canonical-scope.test.ts: 11 failed, 9 passed (20). ["workflow_step_complete","evidence_submit","evidence_submit"] with ["c2","c1","c2"] is emitted unsorted and duplicated; two spellings of one scope emit different arrays; a duplicate changes the signed canonical bytes (randomness, time and the session key are pinned, so the only difference in the signed JSON is the duplicate inside scope); code-unit order ("B" before "a") is not applied; the emitted arrays alias the caller's. - gateway identity-session.test.ts: 3 failed, 1 passed (4). POST /api/identity/session returns the same unsorted, duplicated scope. The passing tests are deliberate characterization and must stay green after the fix: reordering alone is already absorbed by the canonicalizer's sort, an issued key verifies end to end, and keys signed over sorted-but-duplicated arrays (what the old issuer produced) still verify. Agent: implementer-sessionkey Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GihjSLwaMhkBW2DAERqsi1
…scope (#361 E7 F4) Both issuers now build scope.allowedActions and scope.contractIds as [...new Set(xs)].sort() (UTF-16 code-unit order, never localeCompare) BEFORE the SessionKey struct is signed. The emitted object, the bytes the parent signs and any digest taken over the emitted authorization now describe one scope, so one authorization has one digest however the caller spelled the lists. The arrays are fresh copies: a later edit to the caller's array can no longer change an issued key. A scope list that is not an array of strings is refused with a clear Error before anything is signed ("scope.contractIds[1] must be a string, got null"). A bare string is refused too, since spreading it would turn "abc" into ["a","b","c"]. canonicalSessionKeyBytes is deliberately unchanged. It sorts a copy but does not de-duplicate, and it must stay that way: a key signed over sorted-with-duplicates arrays (what the old issuer produced) is verified against exactly those bytes, so de-duplicating there would break keys that are already in the field. A test hand-signs such keys, including the unsorted-with-duplicates shape the old issuer emitted, and pins that they still verify. verifySessionSignedEvent is untouched. Existing test changed, one assertion: test 2 in ephemeral-identity.test.ts passed ["touchstone_response","attestation_sign"] and asserted the same order back, i.e. it pinned the old verbatim emission this fix removes. It is still an exact toEqual with the same unsorted input; the expected array is now the canonical order, with a comment saying why. Tests added to ephemeral-identity-canonical-scope.test.ts: malformed scope rejected by both issuers (8 shapes each) and before the signer is reached; empty lists unchanged; a surrogate pair sorts before U+FF5E (code unit, not code point). The reproduction tests from e84210c now pass: 39/39 in that file, and 39/39 in the existing file. Agent: implementer-sessionkey Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GihjSLwaMhkBW2DAERqsi1
…th 400 invalid_scope (#361 E7 F4) The route validated only principalAgentId and cast the rest of the body, so a scope list that was not an array of strings went straight to issueSessionKey. Now that issueSessionKey refuses such a list (a81e3d3), that would surface as a 500 session_key_issuance_failed. Reproduced first, route unchanged: 5 tests failed with "expected 500 to be 400". A present allowedActions or contractIds that is not an array of strings is now a 400 { error: "invalid_scope", message: "scope.contractIds[1] must be a string" } and issues nothing (no key material in the body). Absent or null lists still mean the defaults and explicit empty lists still mean empty; tests that pass both before and after this change pin both. The scope the route returns is canonical (sorted, de-duplicated) because issueSessionKey now makes it so; the route needed no change for that, and the route tests from e84210c pass (13/13 in the file). The route's verify and revoke handlers are untouched. Agent: implementer-sessionkey Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GihjSLwaMhkBW2DAERqsi1
…nd canonicalization read different values canonicalScopeList validated value[i] by index, then built the scope from `new Set(value)`, which reads the caller's array a second time through its iterator. A direct service caller (JSON cannot carry these) can supply a custom Symbol.iterator, or an index accessor that changes between reads, and get an unvalidated element signed. Three tests (iterator, accessor, allowedActions) fail at 7528bb1: 3 failed | 39 passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GihjSLwaMhkBW2DAERqsi1
…hot (astra pack 145 F1) canonicalScopeList now reads each indexed element exactly once into a local, validates it, and copies it into a fresh snapshot; only the snapshot is de-duplicated and sorted, so what is signed is exactly what was validated. An array carrying its own Symbol.iterator is refused outright (defense in depth). Accessors that change between reads can no longer smuggle an unvalidated element into the signed scope. The canonicalizer and verification are unchanged. Scope tests 42/42 (3 failed at 7528bb1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GihjSLwaMhkBW2DAERqsi1
This branch has not been deployed
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.
What
issueSessionKey(andderiveSessionKey, which had the same defect) now emit a canonical scope:allowedActionsandcontractIdssorted in UTF-16 code-unit order (JS default sort, neverlocaleCompare) and deduplicated, built before signing. So the signed bytes and the emittedSessionKeyobject are the same form.POST /api/identity/sessionanswers a malformed scope (a present list that is not a string array) with400 invalid_scope.Why
Evidence's #361 (EvidenceBlockV2) commits
sessionKeyAuthDigest = sha256(canonicalize(SessionKeyAuthorization)). Cross-family review E7 F4 found that one authorization could have several digests: the issuer emitted the caller's arrays verbatim, while the signature already covered a sorted copy, and that copy kept duplicates. Evidence's ask: #4668. Gateway's answer: #4670.Not changed (deliberately)
canonicalSessionKeyBytesand signature verification are unchanged. Deduping in the canonicalizer would break keys already signed over sorted-with-duplicates arrays; a mutation proves it, and a test pins that legacy keys still verify.Round 2 (astra pack 145, SHIP-WITH-FIXES, F1 MEDIUM)
value[i]by index, but canonicalization consumednew Set(value)(the iterator). A direct caller could slip an unvalidated element in through a custom iterator or a changing accessor. Reproduced (3 failing at 7528bb1); fixed in a762445: each element is read once into a validated snapshot, and an ownSymbol.iteratoris refused. 2 of 2 mutants killed; verifier 30 files / 627; gateway route 13/13.Evidence
localeCompare("B" vs "a", and a surrogate pair vs U+FF5E);ephemeral-identity.test.tstest 2 pinned the old verbatim (unsorted) emission. It still uses an exacttoEqual, now with the canonical order.🤖 Generated with Claude Code
https://claude.ai/code/session_01GihjSLwaMhkBW2DAERqsi1