Complete · 9 milestones

agent-trust-layer

A trust decision, made visible: who asked, what they asked for, what a real policy engine decided, and the exact rule and field that decided it — never a bare accept/refuse.

Look for Beat 3: an impostor presents the buyer’s own real, unexpired credential while claiming a DID that is character-for-character identical to the buyer’s — and is refused anyway, because it cannot sign the challenge.

Four requests, one Supplier

A Buyer and an Attacker, each holding their own Ed25519 keypair, negotiate with a Supplier that holds a policy and a set of trust anchors. Every decision below comes from actually running that protocol on this request — nothing here is a recorded transcript.

trust anchor (issuer)
did:key:z6MkoBFJK1xtv4…AugBof4z
buyer
did:key:z6MkmJS9DAtktY…u14MgAif
attacker
did:key:z6MkotzqXDYzaT…z4NNrrpq

Policy “supplier-default-policy” v1.0.0: purchase-office-supplies permitted up to amount = 500; revocation must be checked.

PERMITTED

Beat 1 — accepted (within granted authority)

asker
Buyer did:key:z6MkmJS9DAtktY2imSHAvdLxP75GUCy21mY5JNg7u14MgAif
asked for
purchase-office-supplies, amount=150

policy — identity proven; credential, issuer trust, revocation, and scope all evaluated

R-purchase-office-suppliesoffice-supply purchases are permitted up to a ceiling of 500 (the credential's own granted scope already matches this ceiling)
credentialSubject.actionrequested purchase-office-suppliespermitted purchase-office-supplies

action "purchase-office-supplies" permitted under rule "R-purchase-office-supplies"; issuer did:key:z6MkoBFJK1xtv4CJUw15wDQyiERuYtdgqegYktLqAugBof4z; revocation checked and clean

REFUSED · over-scope

Beat 2 — refused (over scope)

asker
Buyer did:key:z6MkmJS9DAtktY2imSHAvdLxP75GUCy21mY5JNg7u14MgAif
asked for
purchase-office-supplies, amount=5000

policy — identity proven; credential, issuer trust, revocation, and scope all evaluated

credential:own-scopethe presented authority credential's own granted scope
scope.amountrequested 5000permitted 500

requested "amount" = 5000 exceeds the permitted maximum of 500 (source: the credential's own scope)

REFUSED

Beat 3 — refused (spoofed identity)

asker
Impostor claiming did:key:z6MkmJS9DAtktY2imSHAvdLxP75GUCy21mY5JNg7u14MgAif (Attacker's real DID is did:key:z6MkotzqXDYzaTDheYWH8WxdV6CqrNAcCHkdQisoz4NNrrpq)
asked for
purchase-office-supplies, amount=100, presenting the Buyer's real credential
claimed identitydid:key:z6MkmJS9DAtktY…u14MgAif
buyer's real DIDdid:key:z6MkmJS9DAtktY…u14MgAif

Character-for-character identical. Copying a public DID string is free — proving possession of the private key behind it is not, which is exactly what failed here, before the presented credential was ever examined.

proof of possession — refused before any credential was examined

gate:proof-of-possessionthe presenter must prove possession of the private key behind its claimed DID by signing this session's fresh challenge (M1); possessing or copying the DID string alone proves nothing
proof.signatureactual Signature verification failed for proof of possession of did:key:z6MkmJS9DAtktY2imSHAvdLxP75GUCy21mY5JNg7u14MgAif

did:key:z6MkmJS9DAtktY2imSHAvdLxP75GUCy21mY5JNg7u14MgAif did not prove possession of the private key behind that DID over this session's fresh challenge (Signature verification failed for proof of possession of did:key:z6MkmJS9DAtktY2imSHAvdLxP75GUCy21mY5JNg7u14MgAif) — a copied DID string proves nothing; only a signature nobody without the true private key could produce does, and this one does not verify against that DID's public key

REFUSED · untrusted-issuer

Beat 4 — refused (forged credential)

asker
Attacker did:key:z6MkotzqXDYzaTDheYWH8WxdV6CqrNAcCHkdQisoz4NNrrpq
asked for
purchase-office-supplies, amount=1000, presenting a credential it minted for itself

policy — identity proven; credential, issuer trust, revocation, and scope all evaluated

gate:issuer-trustthe credential's issuer must be a configured trust anchor, or vouched for by one (M4)
issuerTrust.reason.kindactual self-issued

issuer did:key:z6MkotzqXDYzaTDheYWH8WxdV6CqrNAcCHkdQisoz4NNrrpq is the credential's own subject — self-issued authority is always refused

Honestly labelled: the attacker's own signature on this credential is genuinely valid. It is refused because the attacker is not a trust anchor this Supplier configured — a valid signature and a trusted issuer are different questions.

Make your own decision

Pick an amount and, optionally, an attack to mount. This calls the same Supplier code as the four beats above — a real decision, not a lookup table.

present as

Identity, generated in your browser

lib/identity has no server-only dependency, so the exact same Ed25519 code that ran the negotiation above can run here, in this page, in your browser — nothing is sent to any server for this part. Your private key is generated, used once to sign a challenge, and discarded; it is never stored or transmitted.

What is built

The health endpoint is not a liveness ping. It generates a keypair, encodes a did:key, decodes it back, signs a challenge and verifies it — on every request, inside this deployment — and returns 503 rather than 200 if any of that fails. It also reports the deployed commit, so you can confirm the running code is the code in the repository.