Badge Certificate Authority¶
The capiscio-server includes a built-in Certificate Authority (CA) for issuing trust badges with two identity assurance levels.
Overview¶
The Badge CA:
- Signs trust badges with the server's Ed25519 key
- Issues badges for registered agents only
- Supports IAL-0 (account-based) and IAL-1 (proof-of-possession) modes
- Implements RFC-003 Key Ownership Proof protocol
- Enforces trust level requirements
- Publishes public keys via JWKS endpoint
Identity Assurance Levels¶
| Level | Name | Method | Description |
|---|---|---|---|
| IAL-0 | Account-attested | API Key | Badge issued based on account ownership only |
| IAL-1 | Proof of Possession | Challenge-Response | Badge cryptographically bound to agent's private key |
RFC Compliance
- RFC-002: Trust Badge specification with 5 trust levels
- RFC-003: Key Ownership Proof (PoP) protocol for IAL-1 assurance
┌─────────────────────────────────────────────────────────────┐
│ Badge Issuance Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ Agent ──▶ POST /v1/agents/{id}/badge ──▶ CA signs │
│ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Badge (JWT) │ │
│ │ iss: CA URL │ │
│ │ sub: Agent DID │ │
│ │ level: 1-4 │ │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ Verifier ◀── /.well-known/jwks.json ◀── CA public key │
│ │
└─────────────────────────────────────────────────────────────┘
Trust Levels¶
The CA issues badges at different trust levels based on verification:
| Level | Name | Requirements |
|---|---|---|
| 1 | Registered (REG) | Valid account |
| 2 | Domain Validated (DV) | DNS TXT record verified |
| 3 | Organization Validated (OV) | Legal entity verified |
| 4 | Extended Validated (EV) | Manual security audit passed |
Level 0 (Self-Signed)
Level 0 badges are not CA-issued. They are self-signed by agents using capiscio badge issue --self-sign for development only.
Issuing a Badge¶
The CA supports two badge issuance modes:
IAL-0: Account-Based Issuance¶
Simple badge issuance based on API key authentication. Suitable for internal systems where the caller controls the agent.
curl -X POST https://registry.capisc.io/v1/agents/{did}/badge \
-H "X-Capiscio-Registry-Key: cpsc_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"mode": "ial0",
"domain": "my-agent.example.com",
"badge_ttl": 300,
"badge_aud": ["https://api.example.com"]
}'
Response:
{
"success": true,
"data": {
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
"jti": "badge-uuid",
"subject": "did:web:registry.capisc.io:agents:550e8400",
"trustLevel": "1",
"expiresAt": "2025-12-18T10:05:00Z",
"ial": "0"
}
}
IAL-1: Proof of Possession (RFC-003)¶
Two-phase challenge-response protocol that cryptographically proves the agent possesses the private key for their DID. This binds the badge to a specific key, preventing unauthorized use.
Phase 1: Request Challenge
curl -X POST https://registry.capisc.io/v1/agents/{did}/badge/challenge \
-H "X-Capiscio-Registry-Key: cpsc_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"badge_ttl": 300,
"challenge_ttl": 300,
"badge_aud": ["https://api.example.com"]
}'
Response:
{
"challenge_id": "ch-550e8400-e29b-41d4-a716-446655440000",
"nonce": "dGhpcyBpcyBhIHJhbmRvbSBub25jZQ",
"challenge_expires_at": "2025-12-18T10:05:00Z",
"aud": "https://registry.capisc.io",
"htu": "https://registry.capisc.io/v1/agents/did:key:z6Mk.../badge/pop",
"htm": "POST"
}
Phase 2: Submit Proof & Receive Badge
The agent creates a proof JWS signed with their private key:
{
"cid": "ch-550e8400-e29b-41d4-a716-446655440000",
"nonce": "dGhpcyBpcyBhIHJhbmRvbSBub25jZQ",
"sub": "did:key:z6MkqsZXWXcZbFwUrNXBMZg9uHEAj9Ryz6e1Yx97FtBAqxzu",
"aud": "https://registry.capisc.io",
"htu": "https://registry.capisc.io/v1/agents/did:key:z6Mk.../badge/pop",
"htm": "POST",
"iat": 1734519900,
"exp": 1734520200,
"jti": "proof-uuid"
}
Then submit the proof:
curl -X POST https://registry.capisc.io/v1/agents/{did}/badge/pop \
-H "Content-Type: application/json" \
-d '{
"challenge_id": "ch-550e8400-e29b-41d4-a716-446655440000",
"proof_jws": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9..."
}'
Response (IAL-1 Badge):
{
"success": true,
"data": {
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
"jti": "badge-uuid",
"subject": "did:key:z6MkqsZXWXcZbFwUrNXBMZg9uHEAj9Ryz6e1Yx97FtBAqxzu",
"trustLevel": "1",
"expiresAt": "2025-12-18T10:10:00Z",
"ial": "1",
"cnf": {
"kid": "did:key:z6MkqsZXWXcZbFwUrNXBMZg9uHEAj9Ryz6e1Yx97FtBAqxzu#z6MkqsZ...",
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "qsZXWXcZbFwUrNXBMZg9uHEAj9Ryz6e1Yx97FtBAqxzu"
}
}
}
}
PoP Security Requirements
- Challenge is single-use (replay protection)
- Challenge expires in 5 minutes (default)
- Proof must be signed with the key from the agent's DID
- Subject in badge matches the proven DID (e.g.,
did:key) - Badge includes
cnfclaim with full public key JWK
Via Python SDK¶
import asyncio
from capiscio_sdk import request_badge, TrustLevel
async def get_badge():
return await request_badge(
agent_id="550e8400-e29b-41d4-a716-446655440000",
ca_url="https://registry.capisc.io",
api_key="cpsc_live_xxx",
domain="my-agent.example.com",
trust_level=TrustLevel.LEVEL_2,
)
badge_token = asyncio.run(get_badge())
Via gRPC¶
from capiscio_sdk._rpc.client import CapiscioRPCClient
client = CapiscioRPCClient(address='localhost:50051')
response = client.request_badge(
agent_id="550e8400-e29b-41d4-a716-446655440000",
ca_url="https://registry.capisc.io",
api_key="cpsc_live_xxx",
domain="my-agent.example.com",
trust_level="2",
)
print(response.token)
Badge Claims¶
IAL-0 Badge (Account-Based)¶
{
"jti": "badge-unique-id",
"iss": "https://registry.capisc.io",
"sub": "did:web:registry.capisc.io:agents:550e8400",
"iat": 1734519600,
"exp": 1734519900,
"ial": "0",
"vc": {
"type": ["VerifiableCredential", "AgentIdentity"],
"credentialSubject": {
"domain": "my-agent.example.com",
"level": "1"
}
}
}
IAL-1 Badge (Proof of Possession)¶
IAL-1 badges include the cnf (confirmation) claim per RFC-7800, binding the badge to a specific cryptographic key:
{
"jti": "badge-unique-id",
"iss": "https://registry.capisc.io",
"sub": "did:key:z6MkqsZXWXcZbFwUrNXBMZg9uHEAj9Ryz6e1Yx97FtBAqxzu",
"iat": 1734519600,
"exp": 1734519900,
"ial": "1",
"cnf": {
"kid": "did:key:z6MkqsZXWXcZbFwUrNXBMZg9uHEAj9Ryz6e1Yx97FtBAqxzu#z6MkqsZ...",
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "qsZXWXcZbFwUrNXBMZg9uHEAj9Ryz6e1Yx97FtBAqxzu",
"kid": "did:key:z6MkqsZXWXcZbFwUrNXBMZg9uHEAj9Ryz6e1Yx97FtBAqxzu#z6MkqsZ..."
}
},
"pop_challenge_id": "ch-550e8400-e29b-41d4-a716-446655440000",
"vc": {
"type": ["VerifiableCredential", "AgentIdentity"],
"credentialSubject": {
"domain": "my-agent.example.com",
"level": "1"
}
}
}
Claim Reference¶
| Claim | IAL | Description |
|---|---|---|
jti | 0, 1 | Unique badge identifier |
iss | 0, 1 | CA issuer URL |
sub | 0, 1 | Agent's DID (did:web for IAL-0, did:key for IAL-1) |
iat | 0, 1 | Issued at (Unix timestamp) |
exp | 0, 1 | Expiration (Unix timestamp) |
ial | 0, 1 | Identity Assurance Level |
cnf | 1 | Confirmation claim - Contains key binding per RFC-7800 |
cnf.kid | 1 | Key identifier (DID with fragment) |
cnf.jwk | 1 | Full public key JWK (RFC-8037 format for Ed25519) |
pop_challenge_id | 1 | Reference to PoP challenge (audit trail) |
vc.credentialSubject.domain | 0, 1 | Verified domain |
vc.credentialSubject.level | 0, 1 | Trust level (1-4) |
DID Support
- IAL-0 badges: Use
did:webformat (generated from agent UUID) - IAL-1 badges: Use the proven DID from PoP (typically
did:key) - Agents can register with any DID method in the
didfield
JWKS Endpoint¶
The CA publishes its public key at /.well-known/jwks.json:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
"kid": "capiscio-ca-1705315800",
"alg": "EdDSA",
"use": "sig"
}
]
}
Verifiers fetch this endpoint to validate badge signatures.
Badge Expiration¶
Badges have a short TTL by default (5 minutes per RFC-002). This:
- Limits exposure if a badge is compromised
- Forces agents to regularly check in with the CA
- Allows rapid revocation by disabling agents
Use the Badge Keeper daemon for automatic renewal:
capiscio badge keep \
--agent-id "550e8400..." \
--ca-url "https://registry.capisc.io" \
--api-key "$API_KEY" \
--out ./current-badge.jwt
Key Management¶
Key Generation¶
On first startup, if no key exists at CA_KEY_PATH, the server generates an Ed25519 keypair:
The key is saved in JWK format with restrictive permissions (0600).
Key Storage¶
// ca.jwk (private key - NEVER share)
{
"kty": "OKP",
"crv": "Ed25519",
"x": "base64url-public-key",
"d": "base64url-private-key",
"kid": "capiscio-ca-1705315800",
"alg": "EdDSA",
"use": "sig"
}
Key Rotation¶
To rotate the CA key:
- Generate new key on a secure machine
- Add new key to JWKS (keep old key for overlap period)
- Update server to sign with new key
- Remove old key after all badges signed with it have expired
Key Security
The CA private key is the root of trust. Compromise allows forging badges for any agent.
Agent Lifecycle¶
Registration¶
# Register agent
curl -X POST https://registry.capisc.io/v1/agents \
-H "Authorization: Bearer $API_KEY" \
-d '{"name": "My Agent", "domain": "my-agent.example.com"}'
Badge Issuance¶
Only enabled agents can receive badges:
Disabling Agents¶
Disabling an agent immediately prevents new badge issuance:
curl -X POST https://registry.capisc.io/v1/agents/{id}/disable \
-H "Authorization: Bearer $API_KEY"
Existing badges remain valid until expiry, but no new badges can be issued.
Verification Flow¶
Verifier CA
│ │
│ 1. Receive badge token │
│ │
│ 2. GET /.well-known/jwks.json ──────────▶│
│◀────────────────────────────── JWKS ─────│
│ │
│ 3. Verify signature with CA public key │
│ │
│ 4. Check exp, iss, sub claims │
│ │
│ 5. Accept/reject request │
RFC-003 Security Features¶
The PoP implementation includes multiple security layers:
Rate Limiting¶
Challenge requests are rate-limited per DID:
- Default: 10 challenges per DID per 5 minutes
- Purpose: Prevents brute-force attacks
- Error: HTTP 429 with
rate_limit_exceedederror code
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded: too many challenge requests for this DID"
}
Replay Protection¶
Each challenge can only be used once:
- Challenges are marked as "used" after successful badge issuance
- Attempting to reuse a challenge returns HTTP 403
- Database constraint enforces single-use
Challenge Expiration¶
Challenges have configurable TTL (default 5 minutes):
- Set via
challenge_ttlparameter in seconds - Expired challenges cannot be used for proof
- Automatic cleanup of expired challenges
Proof Validation¶
The proof JWS must satisfy all requirements:
- Signature: Valid Ed25519 signature from DID's key
- Claims: Must include
cid,nonce,sub,aud,htu,htm - Nonce: Must match challenge nonce exactly
- Audience: Must match CA issuer URL
- HTTP binding:
htuandhtmmust match badge endpoint - Expiration: Proof must not be expired
- Key relationship: Key must have "authentication" relationship in DID document
DID Resolution¶
The CA resolves DIDs to verify keys:
- did:key: Extracts public key from DID directly
- did:web: Fetches DID document via HTTPS
- Verification: Ensures key has authentication capability
Agent DID Field¶
Agents in the registry have a did field supporting any DID method:
type Agent struct {
ID uuid.UUID
DID *string // e.g., "did:key:z6Mk...", "did:web:..."
Domain *string
// ... other fields
}
Registration with DID:
curl -X POST https://registry.capisc.io/v1/agents \
-H "X-Capiscio-Registry-Key: cpsc_live_xxx" \
-d '{
"name": "My Agent",
"did": "did:key:z6MkqsZXWXcZbFwUrNXBMZg9uHEAj9Ryz6e1Yx97FtBAqxzu",
"domain": "my-agent.example.com"
}'
Update Agent DID:
curl -X PUT https://registry.capisc.io/v1/agents/{agent-id} \
-H "X-Capiscio-Registry-Key: cpsc_live_xxx" \
-d '{
"did": "did:key:z6MkqsZXWXcZbFwUrNXBMZg9uHEAj9Ryz6e1Yx97FtBAqxzu"
}'
DID Resolution
The PoP protocol uses the DID field to: - Identify which agent is requesting the badge - Resolve the agent's public key for proof verification - Set the badge subject to the proven DID
See Also¶
- API Reference — Badge endpoints
- Trust Model — Trust level explanation
- Badges Guide — Practical examples
- RFC-002 — Badge specification
- RFC-003 — Proof of Possession protocol