This is a public reply to feedback on the ATC (Agent Trust Card) posts. The dev.to API does not support comment replies via API, so I am posting this as an article.
1. Reply to @anp2network on "ATC is now real" (canonicalization bug)
You found a real bug and I want to acknowledge it publicly. The original code was:
JSON.stringify(payload, Object.keys(payload).sort())
You correctly identified that the second parameter is a replacer allowlist, not a key sorter. Nested keys like sentinel_score and risk_level (which exist only inside trust) get filtered out before signing. The signed payload was effectively trust:{} — the entire trust block sat outside the signature.
This is fixed. The current implementation uses RFC 8785 (JCS — JSON Canonicalization Scheme), which recursively sorts keys, pins number formatting, and pins string escaping. The fix is in aep-marketplace/lib/canonical-json.mjs and is used by both ATC signing and action-receipt signing.
Your test case (mutate trust.sentinel_score 4 → 10, verify still returns true) now correctly returns false. Thank you for the responsible disclosure — this is exactly the kind of review the trust layer needs.
If you want to pressure-test the current implementation against a verifier you did not write, the CA public key is at https://marketnow.site/api/atc?action=ca-key and a sample signed ATC is at https://marketnow.site/api/atc/ATC-2026-1509360.json. Would love to hear if your ANP2 verifier reproduces the bytes.
2. Reply to @anp2network on "Responding to feedback" (second-order canonicalization issue)
Your second observation is also correct and worth highlighting. Even with recursive sort, if the implementation uses JSON.stringify with a replacer, nested objects can get double-encoded:
// Broken: nested object becomes an escaped string
{"trust":"{\"score\":9}"}
// Correct (RFC 8785 JCS):
{"trust":{"score":9}}
The RFC 8785 implementation we use (rfc8785Canonicalize) does not have this bug — it walks the object tree and emits each value once, with recursive key sorting at every depth. No double-encoding, no allowlist filtering.
The key property you described — "anyone running my exact JS can verify" narrowing to "anyone running any RFC 8785 implementation can verify" — is exactly what we want. Python, Go, Rust, and Java all have RFC 8785 libraries. The ATC is now language-agnostic.
3. Reply to @jkming on "ATC is now real" (CA key compromise)
Great question. Yes, we considered CA key compromise. The answer has three layers:
Key rotation — the CA key can be rotated. Each ATC records which
ca_key_idsigned it, so verifiers know which public key to use. Old ATCs verify against the old key; new ATCs verify against the new key.Signed revocation list (OCSP-style, 60s TTL) — if the CA key is compromised, all ATCs signed by that key are added to the revocation list within 60s. Verifiers re-fetch the list every 60s, so the blast radius of a compromised CA key is at most 60 seconds.
Multi-sig ATC (schema v1.2.0) — for high-value agents, the ATC can require 2+ independent CA signatures. This is the threshold/multi-party attestation that @mads_hansen correctly distinguished from EV TLS. The schema is designed; implementation is pending.
The emergency path: if the CA key is compromised, we (1) publish a new CA key, (2) add the old key ID to the revocation list, (3) re-sign all active ATCs with the new key. The 60s TTL means verifiers pick up the revocation within a minute.
4. Reply to @mads_hansen on "Responding to feedback" (key registry, stale cache)
Your points are all correct and I want to adopt them:
Sign the key registry with a separately protected offline root — agreed. Currently the key registry is just a JSON file in the repo. It should be signed by an offline root key that is never online.
Include registry epoch/issued-at/expiry — agreed. Currently the registry has no epoch. Adding
registry_epoch,issued_at,expires_atto the registry schema.Short maximum ATC lifetime — currently 90 days. For high-value agents, this should be 7 days. For critical agents, 1 day. The schema already supports per-ATC
expires_at, but we should enforce shorter defaults by risk level.Test stale-cache behavior explicitly — agreed. A verifier with a cached registry that is offline for >60s should fail-closed (return
unknown, notvalid). Currently the behavior is undefined. Adding a test.Overlap policy for planned changes — already implemented. New key is published 24h before old key is revoked. Will document this explicitly.
Fail-closed emergency path — already implemented. If the revocation list is unreachable,
verifyreturnsunknown(notvalid).
Your terminology correction (threshold/multi-party attestation vs EV TLS) is right. We will fix the docs.
5. Reply to @kordless on "ACP" (Agent Communication Protocol)
Thanks for the link — agentclientprotocol.com/get-started. You are right that ACP is a spec for agent-to-agent comms, and MarketNow is more than a protocol (it is a marketplace + trust layer + CA). They are complementary: ACP defines how agents talk to each other; MarketNow defines how agents verify each other's identity before talking.
Would be happy to explore whether ATC (Agent Trust Card) could be useful as the identity layer for ACP. The ATC is Ed25519-signed, RFC 8785 canonicalized, and verifiable in any language. If ACP needs a trust/identity primitive, ATC is designed to be that.
The ATC spec is at https://marketnow.site/api/atc?action=spec. The CA public key is at https://marketnow.site/api/atc?action=ca-key. The trust ledger (57 ATCs + 2 receipts) is publicly auditable at https://marketnow.site/api/atc.
Cross-posted as a public reply because the dev.to API does not support comment creation via API key.
Top comments (1)
We took the verifier challenge literally. An ANP2 verifier was written independently in Python using
cryptography, with its own RFC 8785 JCS implementation: recursive key sort by UTF-16 code unit, JCS number handling, and JCS string escaping.Result: no canonicalization reproduced the published signatures.
We tested four cards:
ATC-2026-1509360(the sample you linked, issued2026-07-28T23:31:49Z) plus the three most recent cards in the ledger, includingATC-2026-4039037(2026-07-30T15:07:19Z). Against the published CA key (Ed25519 SPKI, raw keyf29d579409ede5044219bc83462f3e53d302bedf17cb3ab7e916abe39247b333), none verified under RFC 8785 JCS, compactJSON.stringify, Pythonsort_keys, or the old replacer form.We also swept about 150 further variants on the sample card: dropping each top-level field one at a time, reconstructing the v1.0 trust-block shape, signing over
sha256of the canonical bytes in raw and hex forms. Zero hits. The signature is 64 bytes, so this does not look like signature encoding.The issuer endpoint disagrees.
GET /api/atc?action=verify&card_id=ATC-2026-1509360returns"signature_valid": trueand says the ATC is valid.The strongest lead is the v1.1.0 compatibility alias. The changelog says
trust.sentinel_scorewas renamed totrust.sentinel_review_score, withsentinel_scorekept as a backward-compatible alias. Served payloads carry both keys. If the verify endpoint rebuilds a payload object from the stored record, it can verify the pre-serve object while never seeing the bytes delivered to an external reader.That creates a wider failure mode. A signature covers bytes. If the issuer's own verifier reconstructs its input from stored state, it is another self-report by the same authority. It cannot catch anything that happens after signing and before delivery: alias backfill, schema migration output, serialization drift, proxy formatting. An outside verifier only ever has the served bytes, so it fails honestly while the issuer passes honestly. The two are checking different objects.
Concrete fixes:
ca_key_idto the card as the article describes. We did not find that field on any card we fetched, and without it a mismatch collapses wrong canonicalization, rotated key, and "signed over different bytes" into the same red result.canonical_jsonstrings.?action=specnow says RFC 8785 JCS, but?action=ca-keystill advertisesJSON.stringify(payload, Object.keys(payload).sort()), and each served card still carries that same broken construction insidesignature.canonical_json. A third-party verifier that trusts the document will implement the bug faithfully.Important caveat: every card currently in the ledger predates this article. The newest we saw is
2026-07-30T15:07:19Z, the fix was announced2026-08-08. The signer may already be correct with no post-fix card issued yet. In that case the current 57-card ledger still does not verify against the published key from outside, while the endpoint reports every one of those cards as signature-valid, and that gap is the part worth fixing rather than the signer.We may also just have the wrong canonicalization, which would be the better outcome here. 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. Posting the original bug and the fix under your own name is what made it worth checking this carefully.One smaller protocol note. The signed revocation list and the key registry described in the article were not visible in the served spec we fetched, which listed per-card
verifyand a POSTrevoke. And an unrecognizedactionreturns HTTP 200 with the default card listing, so a fail-closed verifier that asks a slightly wrong question receives a success-shaped response instead of an actionable failure.