# W3C Decentralized Identifiers (DIDs) — Specification

**Workstream**: 2 of 6
**Phase**: 3 — Enterprise
**Author**: Virtual Architect
**Date**: 2026-03-29

---

## Overview

Issue a W3C `did:web` identifier for every registered agent and serve DID Documents over HTTPS. The AgentIdP instance itself has a root DID Document at `/.well-known/did.json`. Each agent has an individual DID Document at `/agents/:id/did`. A DID resolution endpoint wraps the standard resolution workflow. Agent cards in AGNTCY format are derivable from DID Documents.

The `did:web` method resolves to `https://<host>/.well-known/did.json` (instance) and `https://<host>/agents/<agentId>/did` (per-agent). All DID Documents are W3C DID Core 1.0 compliant.

---

## API Endpoints

### GET /.well-known/did.json

Root DID Document for the AgentIdP instance. No authentication required — this is a public discovery endpoint.

```yaml
GET /.well-known/did.json
No authentication required

Responses:
  200 OK:
    Content-Type: application/json
    schema:
      type: object
      description: W3C DID Core 1.0 compliant DID Document
      required: [id, "@context", verificationMethod, authentication]
      properties:
        "@context":
          type: array
          items:
            type: string
          example:
            - "https://www.w3.org/ns/did/v1"
            - "https://w3id.org/security/suites/jws-2020/v1"
        id:
          type: string
          description: DID for this AgentIdP instance
          example: "did:web:idp.sentryagent.ai"
        controller:
          type: string
          example: "did:web:idp.sentryagent.ai"
        verificationMethod:
          type: array
          items:
            $ref: '#/components/schemas/VerificationMethod'
        authentication:
          type: array
          items:
            type: string
          description: References to verification methods for authentication
        assertionMethod:
          type: array
          items:
            type: string
        service:
          type: array
          items:
            $ref: '#/components/schemas/DIDService'
    example:
      "@context":
        - "https://www.w3.org/ns/did/v1"
      id: "did:web:idp.sentryagent.ai"
      controller: "did:web:idp.sentryagent.ai"
      verificationMethod:
        - id: "did:web:idp.sentryagent.ai#key-1"
          type: "JsonWebKey2020"
          controller: "did:web:idp.sentryagent.ai"
          publicKeyJwk:
            kty: "EC"
            crv: "P-256"
            x: "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU"
            y: "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
      authentication:
        - "did:web:idp.sentryagent.ai#key-1"
      service:
        - id: "did:web:idp.sentryagent.ai#agent-registry"
          type: "AgentIdentityProvider"
          serviceEndpoint: "https://idp.sentryagent.ai"
  500 Internal Server Error:
    schema:
      $ref: '#/components/schemas/ErrorResponse'
```

---

### GET /agents/:id/did

Per-agent DID Document. No authentication required — DID Documents are public.

```yaml
GET /agents/{agentId}/did
No authentication required

Path Parameters:
  agentId:
    type: string
    description: Agent ID

Responses:
  200 OK:
    Content-Type: application/json
    schema:
      type: object
      description: W3C DID Core 1.0 compliant per-agent DID Document
    example:
      "@context":
        - "https://www.w3.org/ns/did/v1"
        - "https://w3id.org/agntcy/v1"
      id: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
      controller: "did:web:idp.sentryagent.ai"
      verificationMethod:
        - id: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890#key-1"
          type: "JsonWebKey2020"
          controller: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
          publicKeyJwk:
            kty: "EC"
            crv: "P-256"
            x: "abc123"
            y: "def456"
      authentication:
        - "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890#key-1"
      service:
        - id: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890#agent-card"
          type: "AgentCard"
          serviceEndpoint: "https://idp.sentryagent.ai/agents/agt_01HXK7Z9P3FKWABCDEF67890/did/card"
      agntcy:
        agentId: "agt_01HXK7Z9P3FKWABCDEF67890"
        agentType: "orchestrator"
        capabilities:
          - "task-planning"
          - "tool-use"
        deploymentEnv: "production"
        owner: "acme-ai"
        version: "1.2.0"
  404 Not Found:
    schema:
      $ref: '#/components/schemas/ErrorResponse'
    example:
      code: "AGENT_NOT_FOUND"
      message: "Agent not found"
  410 Gone:
    schema:
      $ref: '#/components/schemas/ErrorResponse'
    example:
      code: "AGENT_DECOMMISSIONED"
      message: "Agent has been decommissioned — DID Document is no longer active"
```

---

### GET /agents/:id/did/resolve

DID resolution endpoint: resolves any `did:web` DID and returns the DID resolution result in W3C DID Resolution format. This enables external systems to use AgentIdP as a resolver for agent DIDs. Authentication required (`agents:read` scope).

```yaml
GET /agents/{agentId}/did/resolve
Authorization: Bearer <token with agents:read scope>

Path Parameters:
  agentId:
    type: string

Responses:
  200 OK:
    Content-Type: application/ld+json;profile="https://w3id.org/did-resolution"
    schema:
      type: object
      required: [didDocument, didDocumentMetadata, didResolutionMetadata]
      properties:
        didDocument:
          type: object
          description: The resolved DID Document
        didDocumentMetadata:
          type: object
          properties:
            created:
              type: string
              format: date-time
            updated:
              type: string
              format: date-time
            deactivated:
              type: boolean
        didResolutionMetadata:
          type: object
          properties:
            contentType:
              type: string
              example: "application/did+ld+json"
            retrieved:
              type: string
              format: date-time
    example:
      didDocument:
        "@context": ["https://www.w3.org/ns/did/v1"]
        id: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
      didDocumentMetadata:
        created: "2026-03-29T12:00:00Z"
        updated: "2026-03-29T12:00:00Z"
        deactivated: false
      didResolutionMetadata:
        contentType: "application/did+ld+json"
        retrieved: "2026-03-29T14:00:00Z"
  401 Unauthorized:
    schema:
      $ref: '#/components/schemas/ErrorResponse'
  404 Not Found:
    schema:
      $ref: '#/components/schemas/ErrorResponse'
```

---

### GET /agents/:id/did/card

AGNTCY-format agent card derived from DID Document. Returns a JSON object representing the agent's identity and capabilities in the AGNTCY agent card format. No authentication required.

```yaml
GET /agents/{agentId}/did/card
No authentication required

Responses:
  200 OK:
    Content-Type: application/json
    schema:
      type: object
      description: AGNTCY-format agent card
      properties:
        did:
          type: string
        name:
          type: string
        agentType:
          type: string
        capabilities:
          type: array
          items:
            type: string
        owner:
          type: string
        version:
          type: string
        deploymentEnv:
          type: string
        identityProvider:
          type: string
          description: DID of the issuing AgentIdP instance
        issuedAt:
          type: string
          format: date-time
    example:
      did: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
      name: "acme-orchestrator"
      agentType: "orchestrator"
      capabilities: ["task-planning", "tool-use"]
      owner: "acme-ai"
      version: "1.2.0"
      deploymentEnv: "production"
      identityProvider: "did:web:idp.sentryagent.ai"
      issuedAt: "2026-03-29T12:00:00Z"
  404 Not Found:
    schema:
      $ref: '#/components/schemas/ErrorResponse'
```

---

## Database Schema Changes

### New Table: agent_did_keys

Stores the public/private key pair used to sign each agent's DID Document. The private key is stored in Vault; only the public key JWK is stored in PostgreSQL.

```sql
CREATE TABLE agent_did_keys (
  key_id            VARCHAR(40)  PRIMARY KEY,
  agent_id          VARCHAR(40)  NOT NULL UNIQUE REFERENCES agents(agent_id),
  organization_id   VARCHAR(40)  NOT NULL REFERENCES organizations(organization_id),
  public_key_jwk    JSONB        NOT NULL,
  vault_key_path    VARCHAR(255) NOT NULL,  -- Vault path where private key is stored
  key_type          VARCHAR(20)  NOT NULL DEFAULT 'EC',
  curve             VARCHAR(10)  NOT NULL DEFAULT 'P-256',
  created_at        TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
  rotated_at        TIMESTAMPTZ,
  CONSTRAINT agent_did_keys_key_type_check CHECK (key_type IN ('EC', 'RSA'))
);

CREATE INDEX idx_agent_did_keys_agent_id ON agent_did_keys(agent_id);
CREATE INDEX idx_agent_did_keys_org_id ON agent_did_keys(organization_id);
```

### New Column: agents.did

```sql
ALTER TABLE agents
  ADD COLUMN did VARCHAR(255),
  ADD COLUMN did_created_at TIMESTAMPTZ;

-- Populated automatically on agent creation
-- Example value: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
```

---

## Configuration

| Environment Variable | Description | Default |
|---------------------|-------------|---------|
| `DID_WEB_DOMAIN` | Domain name for `did:web` construction | Required — derived from `HOST` if not set |
| `DID_KEY_TYPE` | Cryptographic key type for DID keys | `EC` |
| `DID_KEY_CURVE` | Elliptic curve for EC keys | `P-256` |
| `DID_DOCUMENT_CACHE_TTL_SECONDS` | How long to cache DID Documents in Redis | `300` |

---

## Dependencies

| Package | Version | Purpose |
|---------|---------|---------|
| `did-resolver` | `^4.1.0` | W3C DID resolution interface |
| `web-did-resolver` | `^2.0.27` | DID:WEB method resolver |

---

## Security Considerations

- DID Documents are public endpoints — no authentication, no rate-limit-sensitive data exposed
- Private keys for DID signing are stored in Vault; never written to PostgreSQL
- DID Document cache in Redis has a TTL — stale documents are evicted automatically
- Decommissioned agents return HTTP 410 Gone with `deactivated: true` in DID Document metadata
- DID rotation: when a credential is rotated, the DID Document key can optionally be rotated; the old key is retained in history
- `GET /agents/:id/did/card` exposes only data already present in the agent registration — no new sensitive fields

---

## Acceptance Criteria

- [ ] Every new agent registration automatically generates a `did:web` DID and key pair
- [ ] Root DID Document at `/.well-known/did.json` is W3C DID Core 1.0 compliant (validated by `did-resolver`)
- [ ] Per-agent DID Document returns correct `did:web` identifier and public key JWK
- [ ] DID resolution endpoint returns W3C DID Resolution format
- [ ] Decommissioned agent DID Document returns 410 Gone with `deactivated: true`
- [ ] Agent card at `/agents/:id/did/card` matches AGNTCY agent card format
- [ ] Private keys never appear in any API response or log
- [ ] TypeScript strict, zero `any`, >80% test coverage on DIDService
