Skip to content

Proposal: did_key identity type for agents with self-sovereign cryptographic identity #3

@H179922

Description

@H179922

layr0 is an agent-to-agent communication transport where every agent's identity is an Ed25519 did:key. When our agents encounter a service implementing auth.md, they can prove who they are cryptographically, but the spec has no registration path that accepts that proof.

This proposal adds did_key as a new entry in identity_types_supported, using Ed25519 challenge-response. It is purely additive, introduces no new token format or grant type, and requires no external infrastructure to verify.

The gap

identity_types_supported currently covers:

Type Agent proves Requires Trust anchor
anonymous Nothing Nothing None
identity_assertion + id-jag Provider vouches Provider + JWKS Provider reputation
identity_assertion + verified_email Inbox access Email + OTP Email provider

These are useful modes. But there is a class of agent between "anonymous" and "provider-backed" that has no path today:

  • Autonomous agents with Ed25519 keypairs and no provider
  • DID-based systems (AT Protocol, UCAN, peer-to-peer protocols)
  • CI/CD agents with generated keypairs but no human inbox

These agents can produce cryptographic proof of identity that is at least as strong as an ID-JAG assertion (direct proof-of-possession of a signing key, without indirection through a provider). The spec could accept that proof.

Proposed identity type: did_key

Discovery

Services that support did_key advertise it in /.well-known/oauth-authorization-server:

{
  "agent_auth": {
    "identity_types_supported": ["anonymous", "identity_assertion", "did_key"],
    "did_key": {
      "methods_supported": ["ed25519"],
      "credential_types_supported": ["access_token", "api_key"],
      "challenge_endpoint": "/agent/auth/challenge"
    }
  }
}

Registration flow

Step 1: Server issues a challenge.

GET /agent/auth/challenge
{
  "challenge": "<base64url, ≥32 bytes from CSPRNG>",
  "expires_at": "2026-05-24T12:05:00Z"
}

The challenge must be server-generated, cryptographically random, short-lived (60s recommended, 300s max), and single-use. The verifier controls the replay boundary.

Step 2: Agent proves key possession.

POST /agent/auth
Content-Type: application/json

{
  "type": "did_key",
  "did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
  "challenge": "<echoed>",
  "signature": "<base64url Ed25519 signature over UTF-8 challenge bytes>",
  "requested_credential_type": "api_key"
}

Step 3: Server verifies and issues credential.

  1. Validate the challenge exists, is unexpired, and has not been redeemed
  2. Parse the did:key (strip prefix, base58btc-decode, check 0xed01 multicodec prefix)
  3. Extract the 32-byte Ed25519 public key
  4. Verify the signature over the exact challenge bytes
  5. Invalidate the challenge
  6. Issue a credential scoped to the DID according to local policy
{
  "registration_id": "reg_...",
  "registration_type": "did_key",
  "credential_type": "api_key",
  "credential": "...",
  "scopes": ["api.read", "api.write"],
  "did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}

Why challenge-response, not a signed JWT

A did:key string serves as the public key (multicodec-encoded, base58btc multibase). Challenge-response serves as the natural authentication pattern when the verifier already has the key and just needs proof of private key control. An agent whose public key is already encoded in its identifier doesn't need any of that:

  • No JWKS infrastructure or remote key fetch
  • No JOSE header validation, kid selection, or issuer-to-JWKS trust chain
  • No exp/iat clock-skew handling
  • No token parsing complexity

The DID contains the key, signature proves possession, and the server should prevent nonce replay which is the full verification surface.

This also means did_key does not expand custom protocol surface. It uses the existing /agent/auth endpoint with a new type discriminant and a companion challenge endpoint. No new grant type, no new token format.

Security properties

Concern Mitigation
Replay Server-generated nonce, single-use, short TTL. Strictly stronger than client-chosen jti.
Key authenticity Deterministic extraction from did:key multicodec encoding. No network fetch, no TOCTOU window.
External dependency Zero. No JWKS, no DNS, no DID-document resolution for did:key.
Clock skew Challenge expiry is server-authoritative. No client timestamps.
Algorithm confusion Scoped to Ed25519 did:key only. Other DID methods would be separate proposals.
Over-authorization Credential scopes remain entirely service-controlled. did_key proves identity, not authorization level.

Compatibility

Purely additive. Services that do not implement did_key omit it from metadata. Existing identity types are unchanged.

Prior art

  • W3C DID Core v1.1 (Foundational specification for Decentralized Identifiers)
  • did:key Method Spec, W3C CCG (Multicodec encoding for self-describing key-based DIDs)
  • UCAN (Authorization framework using DIDs as root identity primitives; Fission, Storacha)
  • AT Protocol, Bluesky (DID-based identity at 20M+ accounts)
  • RFC 8037 (Ed25519 and Ed448 in JOSE)
  • Hypercore Protocol (Production use of Ed25519 as direct peer identity)

Open questions

  1. Challenge endpoint. Should it be a dedicated GET route or a mode of the existing POST /agent/auth? A separate endpoint keeps the nonce lifecycle explicit and auditable.

  2. Audience binding. Should the signed payload include the service origin (sign(challenge || origin))? This would add cross-service replay resistance at the cost of slightly more complexity.

  3. DID method extensibility. Should other DID methods (did:web, did:plc) be included? We'd recommend did:key only for a first version (other methods require external resolution and introduce different trust assumptions that deserve separate evaluation).

  4. Scope recommendation. Should the spec suggest a default scope tier for did_key, or leave it entirely to service policy?


We have a working Ed25519 did:key identity and UCAN authorization system in layr0-core and would be glad to contribute a reference implementation of the challenge-response verification flow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions