Skip to content

fix(golden): sort challenge-id canonical JSON by code point, not locale#54

Open
MattewGraham wants to merge 2 commits into
tempoxyz:mainfrom
MattewGraham:fix/challenge-id-canonical-sort
Open

fix(golden): sort challenge-id canonical JSON by code point, not locale#54
MattewGraham wants to merge 2 commits into
tempoxyz:mainfrom
MattewGraham:fix/challenge-id-canonical-sort

Conversation

@MattewGraham

Copy link
Copy Markdown

Challenge-ID canonicalization: golden adapter sorts keys with locale collation

The challenge id is base64url(HMAC-SHA256(secret, realm|method|intent| base64url(canonical_json(request))|expires|digest|opaque)). For that HMAC to
be a cross-SDK identifier, canonical_json has to be byte-identical
everywhere. It isn't:

Adapter Key ordering
golden (TypeScript) localeCompare — locale/ICU collation
python json.dumps(sort_keys=True) — code point
go json.Marshal map ordering — byte order
rust sort_by_key — byte order
ruby sort_by { key } — byte order
java ORDER_MAP_ENTRIES_BY_KEYS — code-unit order

Five implementations agree on code-point order; the reference is the odd one
out. Locale collation interleaves cases (amount < Currency < network <
ZONE) where code-point order groups uppercase first (Currency < ZONE <
amount < network), so any request object with mixed-case keys canonicalizes
differently — and therefore HMACs differently — on the golden adapter.

Demonstrated with {"amount", "Currency", "ZONE", "network": {"chainId", "Layer"}}:

golden (before):  M03h0VGUCjTNYrvEcVpYNNKeU4CcVJ9d6o0umK8IWuU
python:           WXjRL_umEnaXW00KDPpLZrhhlolHNkHK_I5y-cIShdE   ← matches independent recomputation

Worse than plain divergence: localeCompare output depends on the runtime's
ICU data and default locale, so the reference implementation's "canonical" ids
aren't even guaranteed reproducible across machines. Every existing
challenge-id.json vector happens to use lowercase-initial keys, which is the
only reason this never surfaced.

Fix

Sort with a plain code-point comparator in stableStringify (used exclusively
by generateConformanceChallengeId):

.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))

New vector

Added mixed_case_key_ordering to vectors/challenge-id.json (mixed-case
keys at both the top level and inside a nested object, expected id
WXjRL_…). It fails against the unfixed golden adapter and passes after the
fix, so the suite now pins this behavior for every SDK. The expected value was
computed independently (Python hmac/hashlib reimplementation, which also
reproduces all 25 existing vector ids) and cross-checked against the python
adapter.

Results

  • vector_runner.py --adapter typescript --adapter python --vector challenge-id:
    52/52 pass (26 scenarios × 2 adapters) after the fix.
  • Full typescript vector suite passes; stableStringify has no other call sites.

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.

1 participant