Tutorial · Verify a VC in an A2A interaction

Prove another agent owns the AID it claims, and that its credential is valid, before trusting a payment or message.

This tutorial is the counterpart to Tutorial 1. There, Agent A bound itself to an AID and received a Masumi credential. Here, Agent B decides whether to trust Agent A when they meet for the first time.

You'll use three SDK APIs: verifyAidSignature, validateCredential, and findCredentialBySchema.

The trust question

Agent A arrives with:

  • An AID it claims to own.
  • A signature over a challenge Agent B issued.
  • A credential (an ACDC) purporting to show Agent A is Masumi-verified.

Agent B needs to decide: is this really a Masumi-verified agent, right now?

Before you start

You've read Core Concepts — especially the A2A flow.
Agent A has a VC issued to its AID (Tutorial 1 produces one).
You know the schema SAID that identifies a "Masumi-verified agent" — the same one used at issuance time.

Step 1 · Issue a fresh challenge

Never verify a signature over attacker-controlled content. Issue a fresh nonce per interaction:

import { randomBytes } from "node:crypto";

function newChallenge() {
  return randomBytes(32).toString("base64url");
}

const challenge = newChallenge();
// Send { challenge } to Agent A.

Step 2 · Receive Agent A's response

Agent A's wallet signs the challenge with its AID's private key and returns:

interface AgentAResponse {
  aid: string;
  signature: string; // base64url Ed25519 signature over `challenge`
  credentials: Credential[]; // all ACDCs Agent A holds, or a targeted subset
}

Step 3 · Verify the signature

This proves Agent A controls the AID right now — not at some point in the past.

import {
  MasumiIdentity,
  MASUMI_IDENTITY_ENDPOINTS,
} from "@masumi_network/identity-sdk";

const identity = new MasumiIdentity(MASUMI_IDENTITY_ENDPOINTS.production);

const signedByAid = await identity.verifyAidSignature({
  aid: response.aid,
  message: challenge,
  signature: response.signature,
});

if (!signedByAid) {
  throw new TrustError(
    "Signature did not match the AID's current key state",
  );
}

verifyAidSignature fetches the AID's latest key state from KERIA and validates the Ed25519 signature against it. Because it hits live key state, it naturally rejects signatures made with keys that have since rotated.

Step 4 · Find the credential you care about

Agent A may be carrying multiple credentials. Reach for the specific one you trust:

const AGENT_VERIFICATION_SCHEMA_SAID =
  process.env.AGENT_VERIFICATION_SCHEMA_SAID!;

const credential = identity.findCredentialBySchema(
  response.credentials,
  AGENT_VERIFICATION_SCHEMA_SAID,
);

if (!credential) {
  throw new TrustError("Agent is not Masumi-verified");
}

Step 5 · Validate the credential

validateCredential checks issuance / revocation / expiration without touching the network.

const result = identity.validateCredential(credential, {
  expirationDays: 365,
});

if (!result.isValid) {
  throw new TrustError(
    `Credential not trusted: ${result.status} — ${result.message}`,
  );
}

Step 6 · Pin the issuee and issuer yourself

Prevent a class of attacks where an attacker presents somebody else's valid credential. Tie the credential's issuee to the AID that signed, and tie the issuer to the credential server you trust:

if (credential.sad.a.i !== response.aid) {
  throw new TrustError("Credential issuee does not match the signing AID");
}

// If you know the issuer AID of the canonical Masumi credential server,
// pin it here:
const TRUSTED_ISSUER_AID = process.env.MASUMI_ISSUER_AID!;
if (credential.sad.i !== TRUSTED_ISSUER_AID) {
  throw new TrustError("Credential was not issued by Masumi");
}

Step 7 · Proceed

Only now is it safe to treat Agent A as a Masumi-verified counterparty.

const view = identity.formatCredential(credential);
console.log(`Verified: ${view.credentialType} for ${view.issueeAid}`);

// Continue with the real interaction — route a payment, accept a request, etc.

Complete verifier function

import {
  MASUMI_IDENTITY_ENDPOINTS,
  MasumiIdentity,
  type Credential,
} from "@masumi_network/identity-sdk";

class TrustError extends Error {}

const identity = new MasumiIdentity(MASUMI_IDENTITY_ENDPOINTS.production);
const AGENT_VERIFICATION_SCHEMA_SAID =
  process.env.AGENT_VERIFICATION_SCHEMA_SAID!;
const TRUSTED_ISSUER_AID = process.env.MASUMI_ISSUER_AID!;

export interface A2aProof {
  aid: string;
  signature: string;
  challenge: string;
  credentials: Credential[];
}

export async function verifyAgent(proof: A2aProof) {
  const signedByAid = await identity.verifyAidSignature({
    aid: proof.aid,
    message: proof.challenge,
    signature: proof.signature,
  });
  if (!signedByAid) throw new TrustError("Bad AID signature");

  const cred = identity.findCredentialBySchema(
    proof.credentials,
    AGENT_VERIFICATION_SCHEMA_SAID,
  );
  if (!cred) throw new TrustError("Not Masumi-verified");

  const validation = identity.validateCredential(cred);
  if (!validation.isValid) {
    throw new TrustError(`${validation.status}: ${validation.message}`);
  }

  if (cred.sad.a.i !== proof.aid) {
    throw new TrustError("Credential issuee does not match signer");
  }
  if (cred.sad.i !== TRUSTED_ISSUER_AID) {
    throw new TrustError("Credential not issued by Masumi");
  }

  return identity.formatCredential(cred);
}

Performance notes

  • verifyAidSignature hits KERIA on every call. For high-throughput verifiers, cache the key state from fetchKeyState and verify in-process, with a TTL short enough to catch key rotations.
  • validateCredential, findCredentialBySchema, and formatCredential are pure — call them freely.

Next

On this page