diff --git a/.changeset/neat-sites-knock.md b/.changeset/neat-sites-knock.md new file mode 100644 index 0000000..279f45e --- /dev/null +++ b/.changeset/neat-sites-knock.md @@ -0,0 +1,6 @@ +--- +"@agentcommercekit/ack-id": minor +"agentcommercekit": minor +--- + +Update ack-a2a handshake to include ownership VC exchange diff --git a/demos/identity-a2a/src/agent.ts b/demos/identity-a2a/src/agent.ts index 8a75617..2aef58c 100644 --- a/demos/identity-a2a/src/agent.ts +++ b/demos/identity-a2a/src/agent.ts @@ -18,6 +18,7 @@ import { generateKeypair } from "agentcommercekit" import { createAgentCardServiceEndpoint } from "agentcommercekit/a2a" +import { issueCredential } from "./issuer" import type { AgentCard, AgentExecutor, @@ -35,16 +36,24 @@ import type { DidUri, JwtSigner, Keypair, - KeypairAlgorithm + KeypairAlgorithm, + Verifiable, + W3CCredential } from "agentcommercekit" +type AgentConfig = { + agentCard: AgentCard + algorithm: KeypairAlgorithm + controller: DidUri +} export abstract class Agent implements AgentExecutor { constructor( public agentCard: AgentCard, public keypair: Keypair, public did: DidUri, public jwtSigner: JwtSigner, - public didDocument: DidDocument + public didDocument: DidDocument, + public vc: Verifiable ) {} static async create( @@ -53,11 +62,13 @@ export abstract class Agent implements AgentExecutor { keypair: Keypair, did: DidUri, jwtSigner: JwtSigner, - didDocument: DidDocument + didDocument: DidDocument, + vc: Verifiable ) => T, - agentCard: AgentCard, - algorithm: KeypairAlgorithm = "secp256k1" + config: AgentConfig ) { + const { agentCard, algorithm, controller } = config + const baseUrl = agentCard.url const agentCardUrl = `${baseUrl}/.well-known/agent.json` const keypair = await generateKeypair(algorithm) @@ -78,7 +89,15 @@ export abstract class Agent implements AgentExecutor { colors.dim(Buffer.from(keypair.publicKey).toString("hex")) ) - return new this(agentCard, keypair, did, jwtSigner, didDocument) + const vc = await issueCredential({ + subject: did, + controller + }) + + console.log("Generated sample VC for ownership attestation") + console.log("VC:", colors.dim(JSON.stringify(vc, null, 2))) + + return new this(agentCard, keypair, did, jwtSigner, didDocument, vc) } async onMessageSend( diff --git a/demos/identity-a2a/src/bank-client-agent.ts b/demos/identity-a2a/src/bank-client-agent.ts index 62fc15b..a330a69 100644 --- a/demos/identity-a2a/src/bank-client-agent.ts +++ b/demos/identity-a2a/src/bank-client-agent.ts @@ -2,7 +2,11 @@ import { colors, createLogger, waitForEnter } from "@repo/cli-tools" import { A2AClient, Role } from "a2a-js" -import { getDidResolver, resolveDid } from "agentcommercekit" +import { + getDidResolver, + resolveDid, + verifyParsedCredential +} from "agentcommercekit" import { createA2AHandshakeMessage, createSignedA2AMessage, @@ -12,6 +16,7 @@ import { messageSchema } from "agentcommercekit/a2a/schemas/valibot" import { v4 as uuidV4 } from "uuid" import * as v from "valibot" import { Agent } from "./agent" +import { didResolverWithIssuer, issuerDid } from "./issuer" import { fetchUrlFromAgentCardUrl } from "./utils/fetch-agent-card" import { startAgentServer } from "./utils/server-utils" import type { AgentCard, Message } from "a2a-js" @@ -286,7 +291,8 @@ export class BankClientAgent extends Agent { did: this.did, jwtSigner: this.jwtSigner, alg: this.keypair.algorithm, - expiresIn: 5 * 60 + expiresIn: 5 * 60, + vc: this.vc } ) @@ -299,7 +305,7 @@ export class BankClientAgent extends Agent { const authResponse = await client.sendTask(identityParams) // Step 3: Verify bank teller response - const { nonce: bankNonce } = await verifyA2AHandshakeMessage( + const { nonce: bankNonce, vc: bankVc } = await verifyA2AHandshakeMessage( authResponse, { // Validate that this is intended for our DID @@ -309,6 +315,11 @@ export class BankClientAgent extends Agent { } ) + await verifyParsedCredential(bankVc, { + resolver: didResolverWithIssuer, + trustedIssuers: [issuerDid] + }) + // Check that bank teller included our nonce if (bankNonce !== nonce) { throw new Error("āŒ Bank teller nonce mismatch") @@ -403,13 +414,10 @@ const agentCard: AgentCard = { skills: [] } -if (import.meta.url === `file://${process.argv[1]}`) { - BankClientAgent.create(agentCard, "Ed25519") - .then(async (agent) => { - await agent.requestBankingServices() - }) - .catch((error: unknown) => { - logger.log("Error:", error as Error) - process.exit(1) - }) +export async function getClientAgent() { + return BankClientAgent.create({ + agentCard, + algorithm: "Ed25519", + controller: "did:web:builder.ack.com" + }) } diff --git a/demos/identity-a2a/src/bank-teller-agent.ts b/demos/identity-a2a/src/bank-teller-agent.ts index dc4c1f3..0236daa 100644 --- a/demos/identity-a2a/src/bank-teller-agent.ts +++ b/demos/identity-a2a/src/bank-teller-agent.ts @@ -1,12 +1,13 @@ import { colors, createLogger, waitForEnter } from "@repo/cli-tools" import { Role } from "a2a-js" -import { isDidUri } from "agentcommercekit" +import { isDidUri, verifyParsedCredential } from "agentcommercekit" import { createA2AHandshakeMessage, verifyA2AHandshakeMessage, verifyA2ASignedMessage } from "agentcommercekit/a2a" import { Agent } from "./agent" +import { didResolverWithIssuer, issuerDid } from "./issuer" import { startAgentServer } from "./utils/server-utils" import type { AgentCard, @@ -117,10 +118,18 @@ class BankTellerAgent extends Agent { "Press Enter to verify customer's cryptographic proof..." ) - const { nonce: clientNonce, iss: clientDid } = - await verifyA2AHandshakeMessage(request.params.message, { - did: this.did - }) + const { + nonce: clientNonce, + iss: clientDid, + vc: clientVc + } = await verifyA2AHandshakeMessage(request.params.message, { + did: this.did + }) + + await verifyParsedCredential(clientVc, { + resolver: didResolverWithIssuer, + trustedIssuers: [issuerDid] + }) console.log( colors.yellow( @@ -144,7 +153,8 @@ class BankTellerAgent extends Agent { requestNonce: clientNonce, did: this.did, jwtSigner: this.jwtSigner, - alg: this.keypair.algorithm + alg: this.keypair.algorithm, + vc: this.vc } ) @@ -220,9 +230,13 @@ const agentCard: AgentCard = { } } -export async function startServer() { +export async function startTellerServer() { // Create bank teller agent with did:web instead of did:key - const bankTellerAgent = await BankTellerAgent.create(agentCard, "secp256k1") + const bankTellerAgent = await BankTellerAgent.create({ + agentCard, + algorithm: "secp256k1", + controller: "did:web:bank.com" + }) // Start the server using shared utility return startAgentServer(bankTellerAgent, { @@ -230,11 +244,3 @@ export async function startServer() { port: 3001 }) } - -// Only start server if this file is run directly -if (import.meta.url === `file://${process.argv[1]}`) { - startServer().catch((error: unknown) => { - logger.log("Error starting bank teller server:", error as Error) - process.exit(1) - }) -} diff --git a/demos/identity-a2a/src/issuer.ts b/demos/identity-a2a/src/issuer.ts new file mode 100644 index 0000000..c6e7f84 --- /dev/null +++ b/demos/identity-a2a/src/issuer.ts @@ -0,0 +1,48 @@ +import { + createControllerCredential, + createDidDocumentFromKeypair, + createDidKeyUri, + createJwtSigner, + generateKeypair, + getDidResolver, + signCredential +} from "agentcommercekit" +import type { DidUri } from "agentcommercekit" + +const issuerKeypair = await generateKeypair("Ed25519") + +export const issuerDid = createDidKeyUri(issuerKeypair) +export const issuerDidDocument = createDidDocumentFromKeypair({ + did: issuerDid, + keypair: issuerKeypair +}) + +const signer = createJwtSigner(issuerKeypair) + +const resolver = getDidResolver() +resolver.addToCache(issuerDid, issuerDidDocument) + +export const didResolverWithIssuer = resolver + +export async function issueCredential({ + subject, + controller +}: { + subject: DidUri + controller: DidUri +}) { + const credential = createControllerCredential({ + subject, + controller, + issuer: issuerDid + }) + + const { verifiableCredential } = await signCredential(credential, { + did: issuerDid, + signer, + alg: "Ed25519", + resolver + }) + + return verifiableCredential +} diff --git a/demos/identity-a2a/src/run-demo.ts b/demos/identity-a2a/src/run-demo.ts index 14b29c5..3abdd27 100644 --- a/demos/identity-a2a/src/run-demo.ts +++ b/demos/identity-a2a/src/run-demo.ts @@ -1,6 +1,6 @@ -import { spawn } from "child_process" import { colors, waitForEnter } from "@repo/cli-tools" -import type { ChildProcess } from "child_process" +import { getClientAgent } from "./bank-client-agent" +import { startTellerServer } from "./bank-teller-agent" async function main() { console.log("šŸš€ Starting A2A Bank Identity Verification Demo...\n") @@ -32,9 +32,7 @@ async function main() { ) ) console.log("") - const server: ChildProcess = spawn("tsx", ["./src/bank-teller-agent.ts"], { - stdio: "inherit" - }) + const server = await startTellerServer() await new Promise((resolve) => setTimeout(resolve, 1000)) @@ -48,50 +46,12 @@ async function main() { console.log("") await waitForEnter("Press Enter to start the customer agent...") console.log("") - const client: ChildProcess = spawn("tsx", ["./src/bank-client-agent.ts"], { - stdio: "inherit" - }) - client.on("close", (code: number | null) => { - console.log("") - console.log( - colors.yellow( - "šŸŽ‰ DEMO COMPLETE: Secure Agent-to-Agent Communication Established!" - ) - ) - console.log(colors.yellow(" Key achievements:")) - console.log(colors.yellow(" āœ“ No passwords or shared secrets were used")) - console.log( - colors.yellow(" āœ“ Cross-algorithm compatibility (secp256k1 ↔ Ed25519)") - ) - console.log(colors.yellow(" āœ“ Cryptographic proof of identity ownership")) - console.log( - colors.yellow( - " āœ“ Decentralized identity verification via DID documents" - ) - ) - console.log("\nāœ… Banking demo completed!") - server.kill() - process.exit(code ?? 0) - }) - - client.on("error", (err: Error) => { - console.error("Client error:", err) - server.kill() - process.exit(1) - }) + const bankClientAgent = await getClientAgent() - server.on("error", (err: Error) => { - console.error("Server error:", err) - process.exit(1) - }) + await bankClientAgent.requestBankingServices() - // Handle cleanup - process.on("SIGINT", () => { - console.log("\nšŸ›‘ Shutting down...") - server.kill() - process.exit(0) - }) + server.close() } // Start the demo diff --git a/packages/ack-id/src/a2a/sign-message.ts b/packages/ack-id/src/a2a/sign-message.ts index 7ed844a..7489eb1 100644 --- a/packages/ack-id/src/a2a/sign-message.ts +++ b/packages/ack-id/src/a2a/sign-message.ts @@ -7,6 +7,7 @@ import type { JwtSigner, JwtString } from "@agentcommercekit/jwt" +import type { Verifiable, W3CCredential } from "@agentcommercekit/vc" import type { Message, Role } from "a2a-js" type SignMessageOptions = { @@ -53,6 +54,10 @@ export async function createSignedA2AMessage( } type A2AHandshakeOptions = SignMessageOptions & { + /** + * The verifiable credential to include in the message + */ + vc: Verifiable /** * The nonce of the message we're replying to, if any */ @@ -61,12 +66,12 @@ type A2AHandshakeOptions = SignMessageOptions & { export function createA2AHandshakePayload( recipient: DidUri, - requestNonce?: string + options: A2AHandshakeOptions ) { const nonce = generateRandomNonce() - const nonces = requestNonce + const nonces = options.requestNonce ? { - nonce: requestNonce, + nonce: options.requestNonce, replyNonce: nonce } : { @@ -75,7 +80,8 @@ export function createA2AHandshakePayload( return { aud: recipient, - ...nonces + ...nonces, + vc: options.vc } } @@ -106,39 +112,16 @@ export async function createA2AHandshakeMessage( recipient: DidUri, options: A2AHandshakeOptions ): Promise { - const nonce = generateRandomNonce() - const nonces = options.requestNonce - ? { - nonce: options.requestNonce, - replyNonce: nonce - } - : { - nonce: nonce - } - - const payload = { - aud: recipient, - ...nonces - } + const payload = createA2AHandshakePayload(recipient, options) const { jwt, jti } = await createMessageSignature(payload, options) - const message: Message = { - role, - parts: [ - { - type: "data", - data: { - jwt - } - } - ] - } + const message = createA2AHandshakeMessageFromJwt(role, jwt) return { sig: jwt, jti, - nonce, + nonce: payload.nonce, message } } diff --git a/packages/ack-id/src/a2a/verify.ts b/packages/ack-id/src/a2a/verify.ts index 678d1d2..f0078f3 100644 --- a/packages/ack-id/src/a2a/verify.ts +++ b/packages/ack-id/src/a2a/verify.ts @@ -1,6 +1,7 @@ import { getDidResolver } from "@agentcommercekit/did" import { didUriSchema } from "@agentcommercekit/did/schemas/valibot" import { verifyJwt } from "@agentcommercekit/jwt" +import { credentialSchema } from "@agentcommercekit/vc/schemas/valibot" import { stringify } from "safe-stable-stringify" import * as v from "valibot" import { dataPartSchema, messageSchema } from "./schemas/valibot" @@ -29,7 +30,8 @@ const messageWithSignatureSchema = v.looseObject({ const handshakePayloadSchema = v.object({ iss: didUriSchema, - nonce: v.string() + nonce: v.string(), + vc: credentialSchema }) type VerifyA2AHandshakeOptions = {