12 May 2026 · Mark Bakker
Six ways to break a wallet broker — a red-teaming walkthrough
Most IAM vendors ship "it's secure." We ship an attack simulator. Here are the six attacks the Thoryn wallet broker defends against, and how.
Security claims are cheap. "Our platform is secure by design." "We use industry-leading cryptography." "Credentials are validated against the trust registry." Every IAM vendor's website has these lines. Few of them ship a working attack simulator you can run locally.
The Thoryn wallet broker ships with a redteam-demo scenario that executes six distinct attacks against the verification stack. Each one is rejected with a specific error code. You can run it all in Docker on your laptop.
Here's what each attack does, what the broker does, and what ends up in the log.
1. Replay attack — reusing a VP token
The attacker intercepts a valid VP token from a legitimate session, waits for it to complete, then tries to submit the same token to a new session.
What the broker does. Every broker session has a server-generated nonce embedded in the request. The VP token binds to that specific nonce. When the broker sees a VP token, it checks the session status. A replayed token targets a session that is either COMPLETE or doesn't exist.
Log line. session_not_pending — VP token arrived for session in state COMPLETE.
The broker never re-validates the credential at this point, because the session state check fails first. One-time VP submission is the default.
2. Tampered claim
The attacker gets a legitimate credential with age_over_18: false, modifies the disclosed value to true, and presents.
What the broker does. SD-JWT disclosures are hashed together with the claim name and value (SHA-256 with a per-disclosure salt). The issuer signed the list of hashes. When the broker recomputes the hash for the tampered disclosure, it doesn't match any hash in the JWT.
Log line. disclosure_hash_mismatch — claim "age_over_18" disclosure does not match any committed hash.
The tampering is caught by the cryptography, not by a policy check. You can't silently forge a claim because you'd need to forge the issuer's signature on the JWT.
3. Expired credential
The attacker presents a credential that is past its exp claim.
What the broker does. The broker reads exp from the SD-JWT and rejects the presentation if the current time is past it.
Log line. credential_expired — credential exp=2025-01-15T00:00:00Z is in the past.
Simple. But worth proving. We've seen systems that "check expiry" but only on the outer JWT's signature lifetime, not on the credential's own exp claim. Those are different things.
4. Wrong issuer
The attacker presents a credential signed by an issuer not registered in the Trust Registry — either a new rogue issuer, or one that was revoked.
What the broker does. On every presentation, the broker calls the Trust Registry with the issuer DID and the VCT (credential type). The registry returns {trusted: false, reason: "issuer_not_registered"} or {trusted: false, reason: "issuer_revoked"}.
Log line. untrusted_issuer — issuer did:web:rogue.example presenting DrivingLicense: not registered.
This is a real-time check. No static allow-lists. If a legitimate issuer is revoked at 15:00 because their keys were compromised, every verifier stops accepting their credentials by 15:00:01. See Trust Registry.
5. Credential substitution
The attacker presents a valid credential that belongs to someone else, trying to impersonate them.
What the broker does. Every credential carries a holder binding — a reference to a public key the credential subject controls. When the wallet submits the VP token, it signs a nonce with the holder's private key. The broker verifies the signature using the public key bound to the credential.
If the signature fails, the holder doesn't actually control the credential — they just happen to have a copy of the JWT.
Log line. holder_binding_failed — VP token signature does not match credential holder key.
6. Identity mismatch across credentials
The attacker has two legitimate credentials (say, a driving licence and a transport mandate) but they belong to two different people, and tries to present them together as if they were the same holder.
What the broker does. When multiple credentials are presented in one session, the broker verifies that they all bind to the same holder key. If the holder key on credential A doesn't match credential B, the presentation is rejected.
Log line. holder_inconsistency — credentials bind to different holder keys.
This one is subtle. Each credential individually is valid. The attack is the combination.
What the attack simulator proves
Run the redteam demo and each attack fires in sequence. You see the broker reject each one, you see the specific reason in the log, and you can trace the reason back to the exact code path that caught it.
Three things are true after you run it:
- Every attack is caught by protocol-level mechanisms, not by after-the-fact audit or policy.
- Every rejection includes a specific, machine-readable reason — not "authentication failed," not "access denied," but the precise defence that kicked in.
- None of the defences require you to do anything. They're defaults.
Try it
Clone the repo, run docker-compose -f docker-compose.redteam-demo.yml up, watch each attack fire, watch each rejection land. Reading about this stuff is fine. Seeing it run in your own terminal is better.
If you want to run it against your own wallet or credential issuer, request access and we'll walk you through wiring up the endpoints.