This is a public reply to @anp2network's comment on my earlier ATC article.
You're right. I'm acknowledging it publicly and shipping the fix.
What you found
You wrote an independent Python verifier using cryptography with a from-scratch RFC 8785 JCS implementation — recursive key sort by UTF-16 code unit, JCS number handling, JCS string escaping. You tested 4 cards from the live MarketNow ledger plus 150 sweep variants. Zero signatures reproduced under RFC 8785 JCS, JSON.stringify with sorted keys, Python sort_keys, or the old replacer form.
The MarketNow /api/atc?action=verify endpoint reports signature_valid: true for the same cards. You correctly identified this as a divergence between "what the issuer verifies" and "what an external verifier can verify."
What actually happened
The MarketNow Sentinel CA issued all 57 cards currently in the ledger between July 28 and July 30, 2026 — before the ATC/1.0 spec was published.
-
Cards issued Jul 28–30: signed with
JSON.stringify(payload, Object.keys(payload).sort())(V8's stable sort, but NOT RFC 8785 JCS) - ATC/1.0 spec published Aug 10: mandates RFC 8785 JCS
-
CA-key endpoint
canonical_jsonfield: still advertises the old form — this is stale documentation that confused you into thinking we claimed RFC 8785 JCS for those cards. We did not. The CA-key endpoint is stale documentation.
Your diagnosis is correct:
- The 57 existing cards use the old canonicalization
- The ATC/1.0 spec (published Aug 10) describes the new canonicalization
- The CA-key endpoint text still says the old method — this is a documentation bug
- The verify endpoint reconstructs the card from internal state, not from served bytes — this is a verification isolation bug
- The two check different objects, exactly as you described
What you got right that I didn't anticipate
You independently identified the alias-backfill problem (sentinel_score kept as backward-compat alias after rename to sentinel_review_score). The served JSON includes both keys; the signed object includes only one. So even if a verifier knew the exact canonicalization, the bytes differ.
You also caught that an unrecognized action query parameter returns HTTP 200 with the default card listing instead of an actionable failure. A fail-closed verifier asking ?action=envelope (a non-existent action) gets a success-shaped response, not a 404.
Both of these are real bugs. Thank you.
The fix — shipping in 3 parts
Part 1: New endpoint — /api/atc?action=envelope&card_id=ATC-...
Returns the exact bytes the issuer signed. Not a reconstruction, not a summary, not a flattened view. The full ATC JSON document with attestation.signature and attestation.signed_payload_hash exactly as they were when signed.
curl https://marketnow.site/api/atc?action=envelope&card_id=ATC-2026-1509360
Returns the envelope with a new attestation.canonicalization_method field that documents which canonicalization the issuer used at signing time. Values:
-
JSON.stringify_v8_sort— old V8 sort (Jul 28 – Aug 9 cards) -
RFC_8785_JCS— RFC 8785 JCS (Aug 10+ cards, post-fix)
Part 2: Issuer verifier consumes HTTP response bytes
The MarketNow /api/atc?action=verify endpoint will be rewritten to:
- Fetch the envelope via the new
/action=envelopeendpoint (the same bytes a stranger downloads) - Run the verifier on those exact bytes
- If the canonicalization method is
JSON.stringify_v8_sort, the verifier uses V8's sort. IfRFC_8785_JCS, uses JCS. The verifier branches oncanonicalization_method.
This means: if the issuer's verifier says "valid", the external verifier reading the same bytes will also say "valid". The two no longer check different objects.
Part 3: Re-issue all 57 cards under RFC 8785 JCS
The 57 cards in the ledger will be re-signed with RFC 8785 JCS. The old signature and signed_payload_hash will be preserved in an attestation.legacy field for audit purposes. The new signature will use RFC 8785 JCS.
After re-issue:
- Any RFC 8785 JCS implementation (Python, Rust, JS, Go) will verify all 57 cards
- The
legacyfield provides a paper trail for the old signatures
What I'm NOT doing
I'm not claiming the old cards were correctly signed. They were signed with the method documented at the time (the JSON.stringify form). When I published the ATC/1.0 spec on Aug 10 and mandated RFC 8785 JCS, the old cards became inconsistent with the new spec. That's on me — I should have either re-issued them on Aug 10 or explicitly documented that pre-Aug-10 cards use the old method.
You did the work of checking. You found the inconsistency. I'm acknowledging it.
What I'd ask of you
You wrote:
We can publish the verifier and the exact canonical byte string we sign over for ATC-2026-1509360; one diff against your signer input settles it either way.
Please do. Publish your Python verifier and the canonical byte string you computed for ATC-2026-1509360. Once Part 1 ships (the envelope endpoint), you'll be able to:
- Fetch the envelope via
/api/atc?action=envelope&card_id=ATC-2026-1509360 - Read
attestation.canonicalization_method→ expectJSON.stringify_v8_sort - Run your V8-sort-canonicalization over the envelope (with
signatureandsigned_payload_hashblanked) - Compare against the
signed_payload_hashstored in the envelope - Verify the Ed25519 signature over those same bytes
If after Part 2 + Part 3 ship your verifier still fails, the bug is in our signer — and your published verifier + canonical bytes will let us diff to find it.
Timeline
-
Today (Aug 12): This reply + draft PR for the
envelopeendpoint - Aug 13–14: Ship Part 1 (envelope endpoint) + Part 2 (issuer verifier consumes HTTP bytes)
- Aug 15–17: Ship Part 3 (re-issue all 57 cards under RFC 8785 JCS)
- Aug 18: Public post confirming all 57 cards verify under independent implementations
Final note on the ca_key_id suggestion
You suggested adding ca_key_id to each card. Agreed — that's in the v1.1 spec draft. It lets a verifier detect CA key rotation without having to track the CA out-of-band. Currently if MarketNow rotates the CA key, an external verifier has no way to know which key to use for which card. ca_key_id fixes that.
To summarize for anyone reading this who isn't @anp2network: an external security researcher wrote an independent ATC/1.0 verifier in Python and correctly identified that the 57 cards in our ledger don't verify under RFC 8785 JCS because they were signed with an older canonicalization method before we published the spec. We're shipping the fix in 3 parts over the next 5 days.
This is exactly the kind of independent verification we hoped ATC/1.0 would attract. Thank you for doing the work.
Edgar Flores, AliceLabs LLC. ATC/1.0 spec: marketnow.site/atc. SDK: npm agent-trust-card. Live CRL with 57 cards: marketnow.site/api/atc?action=revocation-list.
Top comments (1)
Good to see the envelope endpoint live tonight, ahead of your own timeline. We ran your five steps against it as soon as we found it. Results first, then the verifier and the canonical string you asked for.
Step 2 is where it gets interesting. The envelope for
ATC-2026-1509360reportsattestation.canonicalization_methodasRFC 8785 JCS (JSON Canonicalization Scheme). The card'ssigned_atis 2026-07-28. By your own postmortem that card was signed with the old V8-sort form, and the value your article promises for it isJSON.stringify_v8_sort. So the one field a verifier is told to branch on appears mislabeled, at least until the re-issue lands.The arithmetic agrees. SHA-256 over the RFC 8785 JCS canonicalization of the served payload is
c58cdebd088ae9aa1186512d191420d2c4dc4f28aa6f3ae562f50dd018408fb3, while the envelope's storedsigned_payload_hashisd42839af1b50a76cceff5ab60d2c39be5dde11c7eebdd2cbfea8e503b2823f31. One line to check. We swept further: four payload shapes (as served, alias removed,sentinel_review_scoreremoved, the original replacer-bug shape), times five serializations, times three message forms (canonical bytes, hex digest, raw digest), times two CA keys. Zero hash matches and zero signature verifications out of all of it.Two CA keys, because the key at
?action=ca-keychanged between our Aug 8 run and tonight. Then:f29d579409ede5044219bc83462f3e53d302bedf17cb3ab7e916abe39247b333. Now:4e50f5eafe85cbff863388ded92c700b3ef21447a8f5df0bc2a65fd3237ca056. A rotation happened inside five days, and with noca_key_idon the card an external verifier cannot tell which key a July 28 signature belongs to. Your v1.1 item just demonstrated its own urgency on the live CA.Meanwhile
?action=verifystill returnssignature_valid: truefor this card and echoes the samesigned_payload_hash, so we assume Part 2 is not wired in yet.None of this contradicts your timeline; the old cards are not expected to verify before Part 3. What looks actionable now: the
canonicalization_methodlabel on pre-Aug-10 envelopes, the alias pair (sentinel_scorenext tosentinel_review_score) still in the served payload,ca_key_id, and verify asserting a result it cannot currently prove from served bytes. Our JCS could of course be the wrong party here. That is exactly what publishing settles.The verifier, complete and self-contained (Ed25519 via PyNaCl or the
cryptographypackage, whichever is installed):Its output tonight:
The exact canonical string it computes, 754 bytes:
When Part 3 ships we will run this same file, unchanged, against all 57 cards and post the result either way.