A2A Protocol is gaining attention as an agent-to-agent communication standard, but authentication remains a practical challenge when implementing real-world systems.
This post explores one possible approach using DID (Decentralized Identifier) signatures to authenticate agents without requiring pre-registration in a central registry. I've built a small experimental library to test this idea, and I'd like to share the code and learnings with the community.
Note: This is an experimental implementation based on
@a2a-js/sdk(compatible with A2A Protocol Specification v0.3.0).
Quick overview
This approach aims to:
- Enable agent authentication without central pre-registration
- Support two trust models:
did:web(HTTPS-based) anddid:ethr(blockchain-based) - Allow cross-domain authentication between different DID methods
Experimental library:
npm install a2a-did
The authentication challenge
When implementing A2A Protocol in practice, a few pain points emerge:
1) OAuth pre-registration overhead
If each agent needs its own identity, you'll need OAuth client registration for every agent created. Sharing the same client_id across agents makes it difficult to distinguish who sent what.
2) Central registry dependencies
Questions arise: Who operates the registry? What happens if it goes down? What if you prefer not to register every agent centrally?
3) Cross-domain authentication complexity
When agents from different organizations need to communicate, verifying the other party's identity can be challenging.
This post explores one possible solution using DIDs (Decentralized Identifiers), with code you can experiment with.
Quick demo (takes ~3 minutes)
Prerequisites
- Node.js 18+
- npm / pnpm / yarn
# Create a new project
mkdir my-a2a-project
cd my-a2a-project
npm init -y
# Install the library
npm install a2a-did tsx
Basic example: Creating a did:web identity
Create demo1.ts:
import { createAgentDIDService } from 'a2a-did';
async function main() {
console.log('=== Creating DID:web Identity ===\n');
const service = await createAgentDIDService(['web']);
const identity = await service.createIdentity({
method: 'web',
agentId: 'my-agent',
config: { type: 'web', domain: 'example.com' }
});
console.log('β Identity created successfully!\n');
console.log('DID:', identity.did);
console.log('Key ID:', identity.keyId);
console.log('Private key length:', identity.privateKey.length, 'bytes');
}
main().catch(console.error);
Run it:
npx tsx demo1.ts
Expected output:
=== Creating DID:web Identity ===
β Identity created successfully!
DID: did:web:example.com:agents:my-agent
Key ID: did:web:example.com:agents:my-agent#key-1
Private key length: 32 bytes
What this approach covers (and what it doesn't)
β What it provides
- Agent authentication without pre-registration (DID-based self-sovereign approach)
- Cryptographic verification (using JWS signatures)
- Cross-domain authentication (agents from different organizations)
-
Two DID methods (
did:webanddid:ethr)
β What it doesn't handle (intentionally out of scope)
1) Delegation/Authorization
This focuses on authentication ("who are you?") rather than authorization ("what can you do?"). Delegation scenarios ("agent A acting on behalf of user B") would likely require Verifiable Credentials or similar mechanisms.
2) Fine-grained access control
DIDs prove identity, but access control policies belong in a separate layer (like a policy engine).
3) Production-ready security
This library provides the signature verification primitives. Production deployments would need additional considerations:
- Replay attack protection (
iat/exp/jtivalidation) - DID Document caching
- Rate limiting and monitoring
For details, see: https://github.com/humuhimi/a2a-did/blob/main/SECURITY.md
Why consider DID signatures?
Challenges with traditional approaches
OAuth 2.0 for dynamic agents:
- Each agent typically needs its own client registration
- Doesn't scale well: 100 agents = 100 registrations
- Less suited for runtime agent creation
Central registries:
- Operational overhead (who runs it, who pays for it?)
- Single point of failure risk
- Privacy concerns about central registration
These concerns have also been discussed in the A2A community, where centralized Agent Registries raise SPOF and potential censorship concerns.
DID-signature approach
Sign A2A messages with agent's private key
β
Distribute public keys via DID Documents
β
Verify signatures β authenticate the agent
This approach aims to reduce pre-registration needs and central registry dependencies, enabling mutual verification between agents as a basic authentication layer.
Note: While the A2A spec describes signing Agent Cards, standardized fields for message-level signatures aren't fully specified yet. This implementation treats message signing as a protocol extension.
Example: did:web signing + verification
About did:web
- Trust model: HTTPS + DNS (leverages existing web infrastructure)
- Setup: Relatively simple (just needs a web server)
- Suited for: Company agents, stable endpoints
β οΈ Important: Production use requires HTTPS. For local testing, consider using tools like mkcert for HTTPS setup, or a resolver configured to allow localhost.
Step 1: Create a DID identity
import { createAgentDIDService } from 'a2a-did';
// Initialize did:web service
const service = await createAgentDIDService(['web']);
// Create DID for agent
const identity = await service.createIdentity({
method: 'web',
agentId: 'my-agent',
config: {
type: 'web',
domain: 'example.com'
// port defaults to 443
}
});
console.log('DID:', identity.did);
// β did:web:example.com:agents:my-agent
Key points:
-
did:webuses HTTPS + DNS as its trust model - DID Document locations:
-
did:web:example.comβhttps://example.com/.well-known/did.json -
did:web:example.com:agents:my-agentβhttps://example.com/agents/my-agent/did.json
-
- Store
privateKeysecurely (type: Uint8Array)
Step 2: Sign an A2A message
import { signA2AMessage } from 'a2a-did';
// Standard A2A Protocol JSON-RPC message
const message = {
jsonrpc: '2.0',
method: 'message/send',
params: {
message: {
kind: 'message',
messageId: 'msg-001',
role: 'user',
parts: [{ kind: 'text', text: 'Hello from Agent' }]
}
},
id: 1
};
// Sign with DID identity
const jws = await signA2AMessage(message, identity);
// Attach signature
const request = { ...message, signature: jws };
Note: The
signaturefield isn't part of the official A2A spec β it's a protocol extension in this implementation. Alternatively, you could transport the JWS via HTTP headers (e.g.,Authorization: DIDAuth <jws>).
Step 3: Verify the signature (receiver side)
import { verifySignedA2ARequest } from 'a2a-did';
// In your server endpoint (Express/Fastify/etc.)
app.post('/a2a', async (req, res) => {
if (req.body.signature) {
const result = await verifySignedA2ARequest(req.body);
if (!result.valid) {
return res.json({
jsonrpc: '2.0',
error: { code: -32600, message: 'Invalid signature' },
id: req.body.id
});
}
console.log(`β
Verified message from: ${result.senderDid}`);
}
// Continue with normal A2A processing...
});
What happens during verification:
- Extract the sender's DID from the signature
- Resolve the DID Document to get the public key
- Verify the JWS signature cryptographically
- If valid,
result.senderDidcontains the verified identity
Alternative: did:ethr for blockchain-based trust
If you prefer not to depend on DNS, did:ethr uses Ethereum as the trust model.
About did:ethr
- Trust model: Ethereum blockchain
- Flexibility: Endpoints can be updated later
- Suited for: Dynamic agents, cross-domain scenarios
Setup example
import { createAgentDIDService } from 'a2a-did';
const service = await createAgentDIDService(['ethr']);
const identity = await service.createIdentity({
method: 'ethr',
agentId: 'blockchain-agent',
config: {
type: 'ethr',
network: 'sepolia', // Testnet
rpcUrl: 'https://sepolia.infura.io/v3/YOUR_INFURA_KEY'
}
});
console.log('DID:', identity.did);
// β did:ethr:sepolia:0x1234567890abcdef...
Agent Card resolution flow
βββββββββββββββββββ
β did:ethr:... β
ββββββββββ¬βββββββββ
β 1. DID Resolution
β
βββββββββββββββββββββββββββ
β DID Document β
β { β
β "service": [{ β
β "type": "A2AAgentCard",
β "serviceEndpoint": β
β "ipfs://Qm..." β
β }] β
β } β
ββββββββββ¬βββββββββββββββββ
β 2. IPFS Fetch
β
βββββββββββββββββββββββββββ
β Agent Card β
β { β
β "a2a_endpoint": β
β "https://agent.../a2a"β
β } β
βββββββββββββββββββββββββββ
Resolving DID β A2A endpoint
import { resolveA2AEndpoint } from 'a2a-did';
// One-step resolution
const endpoint = await resolveA2AEndpoint('did:ethr:sepolia:0x123...');
console.log('A2A Endpoint:', endpoint);
// β https://agent.example.com/a2a
// Now you can communicate
await fetch(endpoint, {
method: 'POST',
body: JSON.stringify(signedMessage)
});
Design rationale:
-
did:ethr + IPFS + HTTPSkeeps identity/metadata on tamper-resistant paths while using HTTPS for actual communication - This is a pragmatic hybrid: blockchain for identity, HTTPS for compatibility with existing infrastructure
- Future iterations could support P2P addresses (e.g.,
/ip4/.../tcp/.../p2p/...)
Comparing did:web vs did:ethr
| Feature | did:web | did:ethr |
|---|---|---|
| Trust model | HTTPS/TLS (Web PKI) | Ethereum blockchain |
| Setup complexity | Lower (HTTPS server) | Moderate (RPC access needed) |
| Resolution speed | Faster (HTTPS) | Slower (blockchain + IPFS) |
| Tamper resistance | Moderate (DNS/HTTPS dependent) | Higher (blockchain guarantees) |
| Operational cost | Low (existing infrastructure) | Low for reads, gas for writes |
| Recommended use | Company agents, stable domains | Dynamic agents, cross-domain |
The choice depends on your threat model and infrastructure preferences.
Summary
This post explored a DID-based approach for A2A Protocol authentication:
- β Agent authentication without central pre-registration
- β Cryptographic verification using JWS
- β
Working examples for
did:webanddid:ethr - β
Compatible with existing A2A implementations (works alongside
@a2a-js/sdk)
Limitations and next steps
This is an experimental library (v0.1.x) focused on exploring the core authentication concept. Production use would require:
- More robust key management guidance
- Performance optimizations (caching, etc.)
- Additional security considerations
I'm working on a full-stack demo to show how this might work in a complete application. Feedback and suggestions are very welcome!
Feedback appreciated π
This is an experimental approach, and I'd love to hear your thoughts:
- π¬ GitHub Discussions - Questions, ideas, feedback
- π GitHub Issues - Bug reports
- π GitHub Repository - Code and documentation
Top comments (0)