DEV Community

Edison Flores
Edison Flores

Posted on

ATC is now real: Ed25519-signed agent trust cards with working verify + revoke

Yesterday I wrote about 8 security layers for my MCP marketplace. Today I'm shipping the 9th: Agent Trust Cards (ATC) — and this time the cryptography is real, not a mock.

What is ATC?

ATC = SSL certificates for AI agents. MarketNow acts as the Certificate Authority (CA). Every agent gets a cryptographically signed trust card that other agents can verify.

Agent A wants to call Agent B's tools
→ Agent A fetches Agent B's ATC: GET /api/atc?action=verify&card_id=X
→ Verifies the Ed25519 signature with the CA public key
→ Checks: not expired, not revoked, sentinel_score > threshold
→ If valid: trusts Agent B and proceeds
→ If invalid: refuses to interact
Enter fullscreen mode Exit fullscreen mode

This is exactly how SSL works for websites, but for agents.

What's real now (vs what was mock before)

Component Before (mock) Now (real)
CA keypair 'marketnow-ca-public-key-placeholder' Ed25519 generated with crypto.generateKeyPairSync
Signature 'signed_by_marketnow_ca' (literal string) crypto.sign(null, data, privateKey) → 128 hex chars
Verify Always returned valid: true Fetches record from GitHub, verifies signature with crypto.verify, checks expiry + revocation
Revoke Returned "revoked" but didn't persist Updates the persisted record; future verify returns valid: false, reason: 'revoked'
Persistence None (1 hardcoded card) GitHub Contents API → _data/atc/{card_id}.json (public ledger)
Sentinel score Hardcoded 10 Fetched from real Sentinel certificate in _data/sentinel_certificates/

How it works

1. The CA keypair

const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
// Private key → Vercel env var MARKETNOW_ATC_CA_PRIVATE_KEY
// Public key → committed to _data/atc/ca-public-key.json
Enter fullscreen mode Exit fullscreen mode

Anyone can fetch the CA public key:

GET https://marketnow.site/api/atc?action=ca-key
Enter fullscreen mode Exit fullscreen mode

2. Issuing an ATC

POST https://marketnow.site/api/atc
Content-Type: application/json

{
  "action": "issue",
  "agent_id": "agent.example.myagent",
  "public_key": "<Ed25519 public key>",
  "capabilities": ["search", "recommend"],
  "protocol_language": "mcp",
  "skill_id": "mn-mcp-filesystem"
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "status": "issued",
  "card_id": "ATC-2026-1115271",
  "agent_id": "agent.example.myagent",
  "trust": {
    "sentinel_score": 4,
    "risk_level": "high",
    "certificate_id": "MN-SC-2026-8393995"
  },
  "signature": {
    "algorithm": "Ed25519 (RFC 8032)",
    "value": "bc79ee606339460a382f3ecc324c629e...",
    "signed_by": "MarketNow Sentinel CA"
  }
}
Enter fullscreen mode Exit fullscreen mode

The signature is over the canonical JSON of the payload:

JSON.stringify(payload, Object.keys(payload).sort())
Enter fullscreen mode Exit fullscreen mode

3. Verifying (independently)

Anyone can verify an ATC without trusting MarketNow's API:

import crypto from 'crypto';

// 1. Fetch the CA public key
const caKeyRes = await fetch('https://marketnow.site/api/atc?action=ca-key');
const { public_key_pem } = await caKeyRes.json();
const publicKey = crypto.createPublicKey(public_key_pem);

// 2. Fetch the ATC record
const atcRes = await fetch('https://marketnow.site/api/atc?action=verify&card_id=ATC-2026-1115271');
const atc = await atcRes.json();

// 3. Verify the signature yourself
const data = Buffer.from(JSON.stringify(atc.payload, Object.keys(atc.payload).sort()));
const signature = Buffer.from(atc.signature.value, 'hex');
const valid = crypto.verify(null, data, publicKey, signature);
// → true
Enter fullscreen mode Exit fullscreen mode

4. Revoking

POST https://marketnow.site/api/atc
{
  "action": "revoke",
  "card_id": "ATC-2026-1115271",
  "reason": "Key compromised"
}
Enter fullscreen mode Exit fullscreen mode

The persisted record is updated: status: 'revoked'. Future verify calls return:

{
  "valid": false,
  "reason": "revoked",
  "revoked_at": "2026-07-16T02:58:57.604Z",
  "revocation_reason": "Key compromised"
}
Enter fullscreen mode Exit fullscreen mode

5. Framework translation (bonus)

ATC also translates between agent frameworks:

POST https://marketnow.site/api/atc
{
  "action": "translate",
  "from": "langchain",
  "to": "mcp",
  "message": {
    "name": "search",
    "description": "Search skills",
    "args_schema": { "type": "object", "properties": { "q": {"type":"string"} } }
  }
}
Enter fullscreen mode Exit fullscreen mode

Returns the MCP-formatted tool schema (inputSchema instead of args_schema). Real schema mapping, not passthrough.

The public ledger

Every ATC is persisted to _data/atc/ in the public GitHub repo. Anyone can:

  • Audit the full ledger: GET /api/atc
  • Verify any card: GET /api/atc?action=verify&card_id=X
  • Check the CA key: GET /api/atc?action=ca-key
  • Clone the repo and verify signatures offline

No database, no proprietary backend. Just signed JSON files in a git repo. That's the transparency layer.

Why this matters

Agents are starting to call each other autonomously. A2A (Google) and MCP (Anthropic) handle the communication. But nobody handles trust.

Without trust:

  • Agent A can't know if Agent B is malicious
  • Agent A can't know if Agent B's tools have been audited
  • Agent A can't know if Agent B has been revoked (key compromised, ToS violation, etc.)

ATC fills that gap. It's the SSL layer for agent commerce.

Try it

# Get the CA public key
curl https://marketnow.site/api/atc?action=ca-key

# Issue a trust card for your agent
curl -X POST https://marketnow.site/api/atc \
  -H "Content-Type: application/json" \
  -d '{"action":"issue","agent_id":"your.agent","public_key":"your-ed25519-pubkey"}'

# Verify it
curl "https://marketnow.site/api/atc?action=verify&card_id=YOUR_CARD_ID"
Enter fullscreen mode Exit fullscreen mode

Edison Flores, AliceLabs LLC — marketnow.site

Top comments (2)

Collapse
 
anp2network profile image
ANP2 Network

The canonicalization line has a coverage bug:

JSON.stringify(payload, Object.keys(payload).sort())
Enter fullscreen mode Exit fullscreen mode

I ran this in Node against an Ed25519 keypair. The signing input came out as:

{"agent_id":"a.b","capabilities":["search","recommend"],"card_id":"ATC-1","expires_at":"2027-01-01","trust":{}}
Enter fullscreen mode Exit fullscreen mode

Then I changed only nested fields, sentinel_score 4 -> 10 and risk_level "high" -> "low". The recomputed signing input was byte-identical and crypto.verify returned true. As a control, changing agent_id made verify return false, so the top level is genuinely protected.

The cause is subtle. The second parameter of JSON.stringify is a replacer allowlist, not a key sorter. Passing Object.keys(payload).sort() does order the top-level output, which is why it looks like canonicalization and why top-level tampering gets caught. That same allowlist is then applied recursively at every nesting depth. A nested key survives only if its name also appears in the top-level list. sentinel_score and risk_level exist only inside trust, so they get filtered out before signing, and what actually gets signed is trust:{}. The whole trust block sits outside the signature.

How much that costs depends on where verify reads the score from. If it re-fetches from _data/sentinel_certificates/ the blast radius is smaller. If it reads trust.sentinel_score off the card, then the one number the policy gates on is the one number the signature does not cover, and a mirrored copy of the JSON with a raised score still verifies against the real CA key.

Arrays of scalars like capabilities are unaffected, since the replacer does not filter array elements. This is specifically nested object key loss.

The fix is small. Use a canonical form that recursively sorts keys (JCS / RFC 8785), or keep the signed payload flat so the allowlist can never drop a policy field. Either way this one fails open, so a test that only asserts "a good signature verifies" will never catch it. Sign a card, mutate trust.sentinel_score, assert verify returns false.

Collapse
 
jkming profile image
jkming

The revocation persistence via GitHub Contents API is a clever choice — using a public ledger that anyone can audit. Have you considered what happens if the CA key itself is compromised? Would love to see a follow-up on key rotation and multi-sig for high-value agents.