Skip to Content
ConceptsTrust Handshake

Trust Handshake

The problem

An agent has an identity and a delegation. But how does it actually prove these to a service?

Just sending the delegation isn’t enough --- anyone who intercepts it could replay it. The service needs to know: “Is this agent the same entity that was delegated these permissions, right now?”

The 3-message protocol

Credat uses a challenge-response handshake inspired by mutual authentication protocols. It proves identity and permissions in exactly three messages:

Service Agent | | | 1. Challenge (nonce) | |----------------------------->| | | | 2. Presentation | | (delegation + proof) | |<-----------------------------| | | | 3. Ack (verified/denied) | |----------------------------->|

Message 1: Challenge

The service creates a challenge containing a random nonce (a one-time-use random value):

import { createChallenge } from "@credat/sdk"; const challenge = createChallenge({ from: "did:web:service.com" }); // { // type: "credat:challenge", // nonce: "dG9rZW4tMTIz...", // from: "did:web:service.com", // timestamp: "2026-02-24T10:00:00.000Z" // }

The nonce prevents replay attacks. Each handshake uses a fresh nonce.

Message 2: Presentation

The agent signs the nonce with its private key and presents it alongside the delegation credential:

import { presentCredentials } from "@credat/sdk"; const presentation = await presentCredentials({ challenge, delegation: delegation.token, agent, }); // { // type: "credat:presentation", // delegation: "eyJhbGci...", // The signed delegation VC // nonce: "dG9rZW4tMTIz...", // Same nonce from challenge // proof: "MEUCIQD...", // Agent's signature over the nonce // from: "did:web:acme.com" // }

Message 3: Verification

The service verifies everything:

import { verifyPresentation } from "@credat/sdk"; const result = await verifyPresentation(presentation, { challenge, // The original challenge ownerPublicKey: ownerKeyPair.publicKey, // Owner who issued the delegation agentPublicKey: agent.keyPair.publicKey, }); if (result.valid) { // Agent is who it claims to be // Delegation is valid and signed by the owner // Scopes tell you what it's allowed to do console.log(result.scopes); // ["email:read", "calendar:write"] }

What gets verified

The verifyPresentation function performs three checks in sequence:

StepWhat it checksFailure means
1. Nonce matchPresentation nonce matches challenge nonceReplay attack or wrong challenge
2. Nonce proofAgent’s signature over the nonce is validAgent doesn’t hold the private key
3. DelegationDelegation VC signature, expiration, revocationInvalid or expired delegation

If any step fails, the result includes specific error codes explaining what went wrong.

Transport agnostic

Credat defines the messages, not the transport. You can send these over:

  • HTTP request/response
  • WebSocket
  • Message queues (RabbitMQ, Kafka)
  • gRPC
  • Even email

The handshake is just three JSON objects. Send them however works for your architecture.

Mutual authentication

The handshake can be made mutual. The ack message optionally includes a counterChallenge for the agent to verify the service’s identity:

interface AckMessage { type: "credat:ack"; verified: boolean; scopes?: string[]; counterChallenge?: ChallengeMessage; // Service proves its identity too delegation?: string; proof?: string; from?: string; }

This enables scenarios where the agent also needs to verify that the service is legitimate before sharing sensitive data.

Error handling

const result = await verifyPresentation(presentation, options); if (!result.valid) { for (const error of result.errors) { switch (error.code) { case "HANDSHAKE_INVALID_NONCE": // Nonce mismatch - possible replay attack break; case "HANDSHAKE_VERIFICATION_FAILED": // Agent's signature is invalid break; case "DELEGATION_SIGNATURE_INVALID": // Delegation wasn't signed by the claimed owner break; case "DELEGATION_EXPIRED": // Delegation has expired break; } } }
Last updated on