Handshake Protocol
The handshake protocol is a structured challenge-response wrapper around the holder-bound SD-JWT presentation flow. It lets a verifier confirm that an agent holds a valid credential and controls the private key bound to it — in a single round trip.
Why a Handshake?
When two agents meet, the verifier needs to answer two questions: does this agent have a valid credential? and is this agent the rightful holder? The handshake answers both by combining credential presentation with holder key proof.
- Single round trip — challenge, present, verify. No separate key-ownership step needed.
- Holder binding — the credential's
cnfclaim binds it to the agent's public key. The KB-JWT in the presentation proves the presenter controls that key. - Replay-resistant — the challenge nonce and audience are bound into the KB-JWT, preventing cross-service and replay attacks.
- Full verification — the verifier checks issuer signature, holder binding, nonce, audience, expiration, revocation, and policy in one call.
The Flow
The handshake is a single round between a Verifier and a Prover (the agent presenting its credential).
1. Create challenge
The verifier generates a challenge containing a cryptographic nonce, an audience identifier (the verifier's own URL or DID), and expiration timestamps controlled by a TTL. The challenge dict looks like:
{
"nonce": "a3f8c1d9...",
"audience": "https://api.verifier.com",
"created_at": "2026-04-02T12:00:00Z",
"expires_at": "2026-04-02T12:02:00Z"
}
2. Respond with presentation
The agent creates a holder-bound SD-JWT presentation. This is a compact string with three parts separated by ~:
<issuer-signed-sd-jwt>~<disclosures>~<holder-signed-kb-jwt>
- The issuer-signed SD-JWT contains the credential claims and the issuer's signature.
- The disclosures are the selectively revealed claims the agent chose to share.
- The KB-JWT (key-binding JWT) is signed by the agent's holder key and binds the challenge nonce and audience into the presentation.
The agent can selectively disclose only the claims the verifier needs (e.g., agentName and capabilities) while keeping others hidden.
3. Verify response
The verifier passes the presentation and the original challenge to verify_response, which delegates to an AgentMarqueVerifier to check:
- Issuer signature — the SD-JWT was signed by a trusted issuer.
- Holder binding — the KB-JWT signature matches the
cnfpublic key in the credential. - Nonce — the KB-JWT contains the expected challenge nonce.
- Audience — the KB-JWT audience matches the challenge audience.
- Dates — the credential is within its validity window and the challenge hasn't expired.
- Revocation — the credential hasn't been revoked (via BitstringStatusList).
- Policy — tier, reputation, and capability requirements are met.
The challenge nonce and audience are bound into the KB-JWT, not just checked as standalone values. This means a presentation created for one verifier cannot be replayed against another.
SDK Usage
The Python SDK provides AgentHandshake with three static methods:
from agentmarque import AgentHandshake, AgentMarqueVerifier
verifier = AgentMarqueVerifier(
trusted_issuers=["did:key:z6MkIssuer..."],
)
# Step 1: create a challenge
challenge = AgentHandshake.create_challenge(
audience="https://api.verifier.com",
ttl=120,
)
# ... send challenge to the agent and receive their presentation ...
# Step 3: verify the agent's response
result = AgentHandshake.verify_response(
response=presentation,
expected_challenge=challenge,
verifier=verifier,
min_tier=1,
required_capabilities=["translate"],
)
if result.valid:
print(result.claims["agentName"])
else:
for err in result.errors:
print(f" - {err}")
from agentmarque import AgentHandshake
# Step 2: respond to the challenge with a holder-bound presentation
presentation = AgentHandshake.respond(
challenge=challenge,
credential=my_credential,
holder_key=my_private_key,
disclose=["agentName", "capabilities", "verificationTier"],
)
# ... send presentation back to the verifier ...