Summary
createSignedA2AMessage signs only { jti, message } with no aud claim (the A2A message schema has no recipient field), and verifyA2ASignedMessage destructures away its did option and calls verifyJwt with only issuer and resolver. Nothing in verification binds the intended recipient. The repo's own tests document this ("Signed messages carry no aud claim today").
Contrast in the same repo
The handshake path does it correctly: createA2AHandshakePayload sets aud: params.recipient and verifyA2AHandshakeMessage passes audience: did to verifyJwt.
Reproduction (source level)
- Agent A signs a message for agent B via
createSignedA2AMessage.
- Agent C obtains the signed message (shared bus, log, relay) and presents it to any service that trusts A.
- The service calls
verifyA2ASignedMessage(msg, { did: <its own did>, counterparty: A }); the did option is ignored, verification succeeds, and C is treated as the intended recipient for the message's 5 minute lifetime.
verifyJwt's missing-audience guard (jwt package verify.ts) only fires when an audience option is supplied, which never happens on this path. Counterparty is bound only if the caller separately passes issuer.
Impact
A message intended for one agent is accepted by any agent or service that trusts the issuer. Recipient-bound authorization decisions built on signed A2A messages rest on an unbound token.
Suggested fix
Include aud in the signed payload (new optional recipient field on the message schema, or a verify-time option) and pass audience to verifyJwt in verifyA2ASignedMessage, mirroring the handshake path. Alternatively document loudly that callers MUST pass issuer and treat verification as sender-authentication only.
Environment: agentcommercekit/ack at commit b5b25ce (main as of 2026-09-12). Reviewed at source level; no live systems were tested.
Disclosure note: reported to security@agentcommercekit.com per SECURITY.md on 2026-09-14; a maintainer asked for these to be filed publicly, so they are here.
Summary
createSignedA2AMessagesigns only{ jti, message }with noaudclaim (the A2A message schema has no recipient field), andverifyA2ASignedMessagedestructures away itsdidoption and callsverifyJwtwith onlyissuerandresolver. Nothing in verification binds the intended recipient. The repo's own tests document this ("Signed messages carry no aud claim today").Contrast in the same repo
The handshake path does it correctly:
createA2AHandshakePayloadsetsaud: params.recipientandverifyA2AHandshakeMessagepassesaudience: didtoverifyJwt.Reproduction (source level)
createSignedA2AMessage.verifyA2ASignedMessage(msg, { did: <its own did>, counterparty: A }); thedidoption is ignored, verification succeeds, and C is treated as the intended recipient for the message's 5 minute lifetime.verifyJwt's missing-audience guard (jwt package verify.ts) only fires when anaudienceoption is supplied, which never happens on this path. Counterparty is bound only if the caller separately passesissuer.Impact
A message intended for one agent is accepted by any agent or service that trusts the issuer. Recipient-bound authorization decisions built on signed A2A messages rest on an unbound token.
Suggested fix
Include
audin the signed payload (new optional recipient field on the message schema, or a verify-time option) and passaudiencetoverifyJwtinverifyA2ASignedMessage, mirroring the handshake path. Alternatively document loudly that callers MUST passissuerand treat verification as sender-authentication only.Environment: agentcommercekit/ack at commit b5b25ce (main as of 2026-09-12). Reviewed at source level; no live systems were tested.
Disclosure note: reported to security@agentcommercekit.com per SECURITY.md on 2026-09-14; a maintainer asked for these to be filed publicly, so they are here.