Implement the Trust Handshake
The handshake combines identity verification and delegation verification into a single protocol. This guide implements the full 3-message flow between an agent and a service.
Setup
import {
createAgent,
generateKeyPair,
delegate,
createChallenge,
presentCredentials,
verifyPresentation,
hasScope,
} from "@credat/sdk";
// --- Owner setup ---
const ownerKeyPair = generateKeyPair("ES256");
const ownerDid = "did:web:acme.com";
// --- Agent setup ---
const agent = await createAgent({
domain: "acme.com",
path: "agents/api-bot",
algorithm: "ES256",
});
// --- Issue delegation ---
const delegation = await delegate({
agent: agent.did,
owner: ownerDid,
ownerKeyPair: ownerKeyPair,
scopes: ["api:read", "api:write"],
});The handshake
Step 1: Service sends a challenge
// --- Service side ---
const challenge = createChallenge({ from: "did:web:service.com" });
// Send `challenge` to the agent (over HTTP, WebSocket, etc.)
// The challenge contains a random nonce that prevents replay attacksStep 2: Agent presents credentials
// --- Agent side ---
// Agent receives the challenge and responds with a presentation
const presentation = await presentCredentials({
challenge, // The challenge from the service
delegation: delegation.token, // The delegation credential
agent, // Agent's full identity (keys + DID)
});
// Send `presentation` back to the serviceStep 3: Service verifies
// --- Service side ---
const result = await verifyPresentation(presentation, {
challenge, // The original challenge we sent
ownerPublicKey: ownerKeyPair.publicKey, // The owner's public key
agentPublicKey: agent.keyPair.publicKey, // The agent's public key
});
if (result.valid) {
console.log("Verified agent:", result.agent);
console.log("Delegated by:", result.owner);
console.log("Scopes:", result.scopes);
// Check specific permissions
if (hasScope(result, "api:write")) {
// Allow write operations
}
} else {
console.error("Verification failed:", result.errors);
}Full example: HTTP API
Here’s a realistic example using an HTTP API:
Agent (client)
async function callProtectedAPI(
agent: AgentIdentity,
delegation: DelegationCredential,
apiUrl: string,
) {
// 1. Request a challenge
const challengeRes = await fetch(`${apiUrl}/challenge`, { method: "POST" });
const challenge: ChallengeMessage = await challengeRes.json();
// 2. Create presentation
const presentation = await presentCredentials({
challenge,
delegation: delegation.token,
agent,
});
// 3. Send presentation with the actual API request
const response = await fetch(`${apiUrl}/data`, {
method: "GET",
headers: {
"X-Credat-Presentation": JSON.stringify(presentation),
},
});
return response.json();
}Service (server)
// Store active challenges (in-memory for simplicity; use Redis in production)
const challenges = new Map<string, ChallengeMessage>();
// Endpoint: Issue challenge
app.post("/challenge", (req, res) => {
const challenge = createChallenge({ from: "did:web:service.com" });
challenges.set(challenge.nonce, challenge);
// Expire after 5 minutes
setTimeout(() => challenges.delete(challenge.nonce), 5 * 60 * 1000);
res.json(challenge);
});
// Middleware: Verify presentation
async function verifyAgent(req, res, next) {
const header = req.headers["x-credat-presentation"];
if (!header) return res.status(401).json({ error: "No credentials" });
const presentation: PresentationMessage = JSON.parse(header);
// Look up the original challenge
const challenge = challenges.get(presentation.nonce);
if (!challenge) return res.status(401).json({ error: "Invalid or expired challenge" });
// Delete challenge (one-time use)
challenges.delete(presentation.nonce);
// Resolve the owner's public key (in production, fetch from DID Document)
const ownerPublicKey = await getOwnerPublicKey(presentation);
// Resolve the agent's public key
const agentPublicKey = await getAgentPublicKey(presentation.from);
const result = await verifyPresentation(presentation, {
challenge,
ownerPublicKey,
agentPublicKey,
});
if (!result.valid) {
return res.status(401).json({ error: "Verification failed", details: result.errors });
}
// Attach verified info to request
req.agent = result;
next();
}
// Protected endpoint
app.get("/data", verifyAgent, (req, res) => {
if (!hasScope(req.agent, "api:read")) {
return res.status(403).json({ error: "Insufficient scopes" });
}
res.json({ data: "secret stuff" });
});Specifying the algorithm
If the agent uses EdDSA instead of ES256, pass the algorithm explicitly:
const result = await verifyPresentation(presentation, {
challenge,
ownerPublicKey,
agentPublicKey,
agentAlgorithm: "EdDSA",
});If not specified, the algorithm is inferred from the public key length:
- 33 bytes = ES256 (P-256 compressed)
- 32 bytes = EdDSA (Ed25519)
Next steps
- Agent-to-agent trust --- Agents authenticating to each other
- Handshake API reference --- Full function signatures
Last updated on