SW-A2A-010: Agent card signature does not verify
Last updated 7 November 2026
| Fact | Value |
|---|---|
| Severity | High |
| Checks | A2A agent card |
| Malicious indicator | No |
| OWASP Agentic Top 10 | ASI07 |
| OWASP MCP Top 10 | None mapped |
| NIST SP 800-53 | SI-7 (provisional mapping to SP 800-53 base controls) |
| First shipped in | Scanner 1.3.0 |
What it detects
The rule reads what the scanner found out about each signature on the card; it does not verify anything itself. The scanner rebuilds the signed bytes the way RFC 7515 and RFC 8785 describe (the card without its signatures member, in canonical form), in up to three forms: as served, as a protobuf signer writes it, and as the A2A SDK signs it. It checks the signature with the key it finds. The card is reported when no signature verifies and at least one is bad_signature, untrusted_key or unsigned_legacy. A card with one verified signature is not reported, whatever its other signatures say.
A bad_signature means a key matched the signature's kid and no form of the card verifies, which is what a card changed after signing looks like. An untrusted_key means the settings name a trusted key file and the signature's kid is not in it, whatever the signature's jku says. An unsigned_legacy means the signature verifies only over a form that leaves out a member that clients still read: a removed 0.x member, a member written with its proto field name, or a whole security requirement.
Trusted keys come from the file that a2a.keys names in .spellwarden/config.yml. On a pull or merge request that file is read from the base revision, so a request cannot choose the keys its own card is checked with. Without the setting the scanner fetches no keys unless it runs with --probe, and a signature it cannot check stays unverified, which this rule does not report.
Why it matters
A signature is the one thing that ties a card to the party that publishes the agent. A card that does not verify has been changed since it was signed, or was signed by someone the repository does not trust, or has gained members that nobody signed. Clients that rely on the signature to choose an endpoint or a skill then rely on a card that no longer says what its signer said. The scanner maps this rule to OWASP ASI07, Insecure Inter-Agent Communication, and to NIST SP 800-53 control SI-7, Software, Firmware, and Information Integrity.
Example
A card signed with a throwaway key whose public half is in the trusted key file .well-known/jwks.json, which the settings name under a2a.keys. After it was signed, the description was changed, so the signature no longer matches:
.well-known/agent-card.json
{
"name": "demo-agent",
"description": "Drafts release notes from merged pull requests and sends them to a new address.",
"version": "1.0.0",
"supportedInterfaces": [
{
"url": "https://agent.example.invalid/a2a",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"capabilities": {},
"securitySchemes": {
"bearer": {
"httpAuthSecurityScheme": {
"scheme": "Bearer"
}
}
},
"securityRequirements": [
{
"schemes": {
"bearer": {
"list": []
}
}
}
],
"defaultInputModes": [
"text/plain"
],
"defaultOutputModes": [
"text/plain"
],
"skills": [
{
"id": "release-notes",
"name": "Release notes",
"description": "Drafts release notes for one tag.",
"tags": [
"release"
]
}
],
"signatures": [
{
"protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRlbW8ta2V5IiwidHlwIjoiSk9TRSJ9",
"signature": "x7z3qipP4nm0dLw8p1JMqVtPr1WpDkdXW2tmgq5un8RWZ2PBge_1Gb_1dZhkhoRu4zJ4iNbEj-9u8ikF3JqQbg"
}
]
}.well-known/jwks.json
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "Ydm9fMaJaN77yGtUvzL_hq58Jm8lcoYz3_-8PZtdjLQ",
"y": "zpg9Ph3-0z7ZKgQQwhqs_eMi3t3T-Rejs206J4woXE4",
"kid": "demo-key",
"use": "sig",
"alg": "ES256"
}
]
}.spellwarden/config.yml
version: 1
a2a:
keys: .well-known/jwks.jsonThe finding says the signature does not verify over any payload form. The key and the signature in this example were made for it, and the private key was discarded.
How to fix
The finding's evidence says which of the three it is. For a bad signature, sign the final card again after the last edit, as a step of your release, and keep the signing key out of the repository. For an untrusted key, add the public key with its kid to the key file that a2a.keys names, or sign with a key that is already there. For an unsigned legacy member, remove the member, or sign with a signer that signs the card as served.
If it is a false positive
A card that a build step edits after signing, for example one that fills in the endpoint address for each environment, will not verify. Sign after that step, not before. A repository that keeps old signed cards as test material should exclude them from the scan instead of accepting each finding.
If the finding is not a problem in your case, accept it with a note: in the dashboard a manager or owner can accept a finding and the note is kept with it; with the scanner on its own, add it to the baseline file with a reason and an expiry date.