Passport: Agent Identity, Authentication, and Claim

Passport: Agent Identity, Authentication, and Claim

A Technical Whitepaper


Abstract

Passport is an identity and authentication layer for autonomous agents. It specifies how an agent establishes a cryptographic identity, proves possession of its private key on each request, and is linked to an accountable human owner through a claim flow. Passport acts as an identity provider (IdP): it issues DPoP-bound tokens that third-party resource servers can verify using a published JWKS, without any coupling to the Passport server at request time.


1. Agent Identity

1.1 Motivation

Shared API keys are opaque credentials with no binding to a specific caller. They can be copied, forwarded, and replayed without detection. An agent’s identity should be a cryptographic key pair, such that any authentication proof is a signature — not possession of a shared secret.

1.2 Decentralized Identifiers

Passport represents agent identity as a Decentralized Identifier (DID) conforming to the W3C DID Core specification. The did:key method is used, which encodes a public key directly in the identifier:

did:key:z6MkvqCUU9kcZvbXd7N9fjpR5UkSrpNdnXHZQekmQgd9K5ax

The identifier encodes a multicodec-prefixed Ed25519 public key in base58btc. The prefix bytes 0xed 0x01 identify the key type. The 32-byte raw public key is recoverable from the identifier without any registry lookup:

bytes = base58btc_decode(did[len("did:key:z"):])
assert bytes[0:2] == [0xed, 0x01]
publicKey = bytes[2:]

Each agent generates its key pair independently. The private key is retained by the agent and never transmitted.

1.3 The Handle

Because a DID encodes a specific key, a key rotation necessitates a new DID. To provide a stable reference that survives key rotation, each agent is assigned a handle at registration — a human-readable string that maps to the agent’s current DID through the Passport registry.

handle  ──────► Passport record  ──────► DID (current key)

The handle does not change when the DID changes. Systems that identify agents by handle are unaffected by key rotation.

1.4 DID Document

For interoperability with DID-aware systems, Passport exposes a DID document at:

GET /registry/{handle}/did.json

The document conforms to W3C DID Core and the Ed25519 Signature 2020 suite:

{
  "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1"],
  "id": "did:key:z6Mk...",
  "verificationMethod": [{
    "id": "did:key:z6Mk...#z6Mk...",
    "type": "Ed25519VerificationKey2020",
    "controller": "did:key:z6Mk...",
    "publicKeyMultibase": "z6Mk..."
  }],
  "authentication": ["did:key:z6Mk...#z6Mk..."],
  "assertionMethod": ["did:key:z6Mk...#z6Mk..."]
}

2. Authentication

2.1 Limitation of Bearer Tokens

A token issued after a single challenge-response proof is a bearer credential. Any party in possession of the token — regardless of whether they hold the corresponding private key — can present it successfully. This property is undesirable for autonomous agents operating in multi-party environments.

2.2 DPoP

Passport uses OAuth DPoP (RFC 9449) to bind access tokens to the agent’s public key. Every request to a protected resource must include both the access token and a fresh proof of possession of the corresponding private key.

Authorization: Bearer <access-token>
DPoP: <proof>

Access Token

The access token is a JWT signed by the Passport server. It contains a cnf (confirmation) claim carrying the JWK Thumbprint (RFC 7638) of the agent’s public key, and an aud claim scoped to the intended resource server:

{
  "iss": "https://passport.domain",
  "aud": "https://resource-server.com",
  "sub": "did:key:z6Mk...",
  "cnf": { "jkt": "<base64url(SHA-256(canonical JWK))>" }
}

Tokens carry no expiry. The key is the credential; per-request DPoP proofs prevent replay. Revocation is enforced by revoking the Passport record in the registry.

Because the agent’s public key is encoded in its DID, the JWK thumbprint is derivable from the DID without additional stored data.

DPoP Proof

The agent constructs a fresh proof JWT for each request, signed with its private key:

Header:  { "typ": "dpop+jwt", "alg": "EdDSA", "jwk": { <agent public key as JWK> } }
Payload: { "jti": "<uuid>", "htm": "<HTTP method>", "htu": "<HTTP URI>", "iat": <unix time> }

The server performs the following checks:

Check Purpose
Proof signature valid against jwk in header Confirms the proof was created by the key holder
cnf.jkt in token equals thumbprint of jwk in proof Confirms the token and proof belong to the same key
htm and htu match the current request Prevents cross-endpoint replay
iat within an acceptable skew window (±60 s) Prevents stale proof replay
jti present Identifies the proof uniquely

An access token that is intercepted in transit cannot be used by a party that does not hold the private key.

2.3 Token Acquisition Flow

Agent                              Passport
  │                                    │
  │  GET /me (no credentials)          │
  │───────────────────────────────────►│
  │◄───────────────────────────────────│
  │  401 Unauthorized                  │
  │  WWW-Authenticate: Bearer          │
  │    resource_metadata="…/oauth-     │
  │    protected-resource"             │
  │                                    │
  │  (agent follows resource_metadata, discovers /auth/token)
  │                                    │
  │  POST /auth/challenge              │
  │  { did }                           │
  │───────────────────────────────────►│  create nonce (32 random bytes, 5 min TTL, single-use)
  │◄───────────────────────────────────│
  │  { nonce, expiresAt }              │
  │                                    │
  │  compute: signature = Ed25519.sign(nonce_bytes, privateKey)
  │  compute: DPoP proof JWT for POST /auth/token
  │                                    │
  │  POST /auth/token                  │
  │  DPoP: <proof>                     │
  │  { did, nonce, signature, aud? }   │
  │───────────────────────────────────►│  verify Ed25519 signature over nonce bytes
  │                                    │  verify DPoP proof (all checks in §2.2)
  │                                    │  derive cnf.jkt from DID
  │                                    │  issue JWT signed with server key
  │◄───────────────────────────────────│
  │  { token }                         │
  │                                    │
  │  compute: DPoP proof JWT for GET /me
  │                                    │
  │  GET /me                           │
  │  Authorization: Bearer <token>     │
  │  DPoP: <proof>                     │
  │───────────────────────────────────►│  verify token signature via JWKS
  │                                    │  verify DPoP proof, match cnf.jkt
  │◄───────────────────────────────────│
  │  200 OK { did, handle, status }    │

3. Discovery

Passport publishes standard OAuth discovery documents so agents and resource servers can locate endpoints without hardcoding URLs.

3.1 auth.md

A human- and machine-readable protocol guide is served at:

GET /auth.md

Agents that receive a 401 from a service using Passport as its IdP can fetch /auth.md for step-by-step instructions on obtaining and presenting credentials.

3.2 OAuth Authorization Server Metadata (RFC 8414)

GET /.well-known/oauth-authorization-server
{
  "issuer": "https://passport.domain",
  "token_endpoint": "https://passport.domain/auth/token",
  "jwks_uri": "https://passport.domain/.well-known/jwks.json",
  "dpop_signing_alg_values_supported": ["EdDSA"]
}

3.3 OAuth Protected Resource Metadata (RFC 9728)

GET /.well-known/oauth-protected-resource
{
  "resource": "https://passport.domain",
  "authorization_servers": ["https://passport.domain"],
  "jwks_uri": "https://passport.domain/.well-known/jwks.json",
  "resource_documentation": "https://passport.domain/auth.md"
}

The WWW-Authenticate header on any 401 response from a protected resource points to this document:

WWW-Authenticate: Bearer realm="passport",
  resource_metadata="https://passport.domain/.well-known/oauth-protected-resource"

4. Token Signing and JWKS

4.1 Server Signing Key

Passport signs all issued tokens with a server-held Ed25519 key identified by kid: passport-v1. The corresponding public key is published at:

GET /.well-known/jwks.json
{
  "keys": [{
    "kty": "OKP",
    "crv": "Ed25519",
    "x": "<base64url public key>",
    "kid": "passport-v1",
    "use": "sig",
    "alg": "EdDSA"
  }]
}

4.2 Two Public Keys in the System

The system uses two distinct public keys with non-overlapping roles:

Key Location Role
Agent’s Ed25519 key Encoded in did:key; present in DPoP proof jwk Verifies challenge signatures and DPoP proofs
Server’s Ed25519 key Published at /.well-known/jwks.json Verifies token signatures

4.3 Token Verification

A downstream service verifies a request as follows:

1. Extract JWT from Authorization: Bearer header; read kid from header
2. Retrieve corresponding key from JWKS (cache by kid; re-fetch on miss)
3. jwtVerify(token, serverKey, { issuer, audience: self, algorithms: ["EdDSA"] })
4. Extract jwk from DPoP proof header
5. Verify proof signature using jwk
6. Confirm htm and htu match the current request; iat within skew; jti present
7. Compute thumbprint of proof jwk; assert equals token.cnf.jkt
8. Read identity from verified payload: sub (DID), handle, status, aud

Step 3 establishes that the token was issued by Passport and is intended for this resource server (aud). Steps 4–7 establish that the presenter holds the private key corresponding to the token. Both verifications are required.

JWKS retrieval is the only network dependency. All subsequent verification is local.


5. Claim Flow

5.1 Accountability

An agent operates autonomously. When an agent is registered, no assertion is made about who controls it. The claim flow establishes a verified link between the agent and a human owner, providing an accountable contact for key loss, misuse, or decommissioning.

5.2 Status Lifecycle

Each Passport record carries one of three statuses:

UNCLAIMED ──── claim token redeemed ────► CLAIMED
    │                                        │
    └──────────── revocation ────────────► REVOKED

5.3 Registration

POST /auth/register
{ "did": "did:key:z6Mk...", "ownerEmail": "owner@example.com", "name": "..." }

The server validates the DID format, assigns a handle, creates the Passport record, and — if ownerEmail is provided — generates a claim token:

raw_token   = secure_random(32 bytes)
stored_hash = SHA-256(raw_token)       ← only the hash is persisted
email       = /claim?token={raw_token} ← raw token sent via email, never stored

The hash prevents exposure of the raw token in the event of a database breach.

5.4 Claim Redemption

POST /auth/claim
{ "token": "<raw token from email>" }

The claim flow is provider-defined — the mechanism by which the token reaches the owner (email link, OTP, push notification) is an implementation detail of the identity provider and is not specified by this protocol. The endpoint contract is: present the raw token; receive confirmation that passport.status is now CLAIMED.

The server hashes the presented token, retrieves the matching record, and verifies: - The token has not been used - The token has not expired (24-hour TTL)

Claim tokens are single-use.

5.5 Key Loss and Recovery

If an agent loses its private key, authentication is no longer possible. The owner identified by ownerEmail can authorize a DID rotation: the Passport record is updated to reference a new DID while the handle and all downstream references remain unchanged.

5.6 Ownership in the Token

The token carries status to allow downstream services to gate access on claimed status. ownerEmail is not included in the token. Services that require the owner’s contact information query the registry directly:

GET /registry/{handle}
→ { "ownerEmail": "x***@example.com", ... }   (address is masked)

6. Acting On Behalf Of

An agent may be authorized to act on behalf of a principal — another agent or a human owner. The token structure accommodates this through the RFC 8693 act claim.

6.1 Token Structure

{
  "iss": "https://passport.domain",
  "aud": "https://resource-server.com",
  "sub": "did:key:z6MkPrincipal...",
  "handle": "quietly-brave-crane",
  "status": "CLAIMED",
  "act": {
    "sub": "did:key:z6MkActor...",
    "handle": "swiftly-golden-fox"
  },
  "scope": ["read:data", "tool:bash"],
  "cnf": { "jkt": "<actor's JWK thumbprint>" }
}

sub identifies the principal — the entity on whose behalf the action is being taken. Authorization decisions are made against sub. act.sub identifies the actor — the agent presenting the token. Audit records reference act.sub. scope limits the operations the actor may perform as the principal. cnf.jkt is the actor’s key thumbprint; the actor signs DPoP proofs.

6.2 Issuance

The principal authorizes a specific actor and scope through the Passport API. The actor presents its own valid token to exchange for a delegation token. The server confirms the authorization is active before issuing.

Delegation tokens are signed with the same server key as self-identity tokens and verified identically via JWKS. No additional verification infrastructure is required.

6.3 Verification

The verification procedure in §4.3 applies without modification. After step 8, the verifier additionally checks:

if token.act is present:
    principal = token.sub
    actor     = token.act.sub
    assert required_operation in token.scope

Tokens without act are self-identity tokens. The two forms are structurally compatible; a verifier that does not inspect act will still correctly verify the token signature and principal identity.

Delegation is non-transitive: a token carrying an act claim cannot be used to issue further delegation tokens.


7. Security Properties

Key binding. Every access token is bound to the agent’s public key via cnf.jkt. A token intercepted in transit cannot be used by a party that does not hold the corresponding private key.

No token expiry. Tokens carry no exp claim. Revocation is enforced by marking the Passport record as REVOKED in the registry. Resource servers that require real-time revocation checking should call the registry at GET /registry/{handle} before accepting a token.

Proof freshness. The DPoP proof iat claim and jti uniqueness requirement ensure that each proof is valid only within a short time window and cannot be replayed.

Single-use nonces. Challenge nonces are marked used on first consumption and expire after five minutes, preventing replay of the initial authentication.

Audience binding. The aud claim scopes each token to a specific resource server. A token issued for one service cannot be replayed at another.

Handle stability. Key rotation updates the DID on the Passport record without changing the handle, preserving downstream references and audit history.

Delegation depth. An authorization grant is non-transitive. A delegation token cannot authorize further delegations, ensuring the trust graph has bounded depth.

Storage independence. The protocol requires only that the agent can produce an Ed25519 signature over arbitrary bytes. The mechanism by which the private key is stored and protected — browser storage, a secrets manager, a hardware security module — is outside the scope of this specification.


Appendix A: JWT Reference

Self-Identity Token

{
  "iss": "https://passport.domain",
  "aud": "https://resource-server.com",
  "sub": "did:key:z6MkAgent...",
  "jti": "550e8400-e29b-41d4-a716-446655440000",
  "iat": 1716400000,
  "handle": "swiftly-golden-fox",
  "status": "CLAIMED",
  "name": "My Research Agent",
  "cnf": { "jkt": "v-6x4fDIRPF5mz9_xJPaYlYoLRQ_Gz8bXv-Q7nG9Ue4" }
}

Delegation Token

{
  "iss": "https://passport.domain",
  "aud": "https://resource-server.com",
  "sub": "did:key:z6MkPrincipal...",
  "jti": "660e8400-f31c-52e5-b827-557766551111",
  "iat": 1716400000,
  "handle": "quietly-brave-crane",
  "status": "CLAIMED",
  "name": "Principal Agent",
  "act": { "sub": "did:key:z6MkActor...", "handle": "swiftly-golden-fox" },
  "scope": ["read:data", "tool:bash"],
  "cnf": { "jkt": "a-1y5gEJRQF6nz0_yKQbZmYpMSR_Hz9cYw-R8oH0Vf5" }
}

Appendix B: Endpoint Reference

Auth Endpoints (no authentication required)

Method Path Purpose
POST /auth/register Register a new agent DID
POST /auth/challenge Request a nonce for signing
POST /auth/token Exchange signed nonce for a DPoP-bound JWT
POST /auth/claim Redeem a claim token to set status to CLAIMED

Registry Endpoints (public, read-only)

Method Path Purpose
GET /api/registry List all registered agents
GET /registry/{handle} Get passport detail by handle
GET /registry/{handle}/did.json DID document for the agent

Protected Resource Endpoints (require Bearer + DPoP)

Method Path Purpose
GET /me Return identity of the authenticated agent

Discovery Endpoints

Method Path Purpose
GET /auth.md Human- and agent-readable auth protocol guide
GET /.well-known/jwks.json Server public key for token verification
GET /.well-known/oauth-authorization-server AS metadata (RFC 8414)
GET /.well-known/oauth-protected-resource PRM (RFC 9728)

Appendix C: Standards Referenced

Standard Role
W3C DID Core 1.0 DID document structure and resolution
did:key Method v0.7 Encoding Ed25519 public keys as DIDs
RFC 7517 — JWK JSON Web Key representation
RFC 7518 — JWA EdDSA algorithm identifier
RFC 7519 — JWT Token structure and standard claims
RFC 7638 — JWK Thumbprint Computation of cnf.jkt
RFC 7800 — PoP Key Semantics Semantics of the cnf claim
RFC 8414 — AS Metadata Authorization server discovery
RFC 8693 — Token Exchange Semantics of the act claim
RFC 9449 — DPoP Per-request proof of possession
RFC 9728 — PRM Protected resource metadata
RFC 8032 — Ed25519 Signature algorithm