Skip to content

PKI

How principals (operator, device, server) obtain the keys and Directory-signed tokens they need. Identity is token-only — a Directory-signed Ed25519 token, with no per-app X.509 client certificate. Certificates appear in exactly one place: transport TLS (server-auth), including the mutually-authenticated server↔server federation link — see Trust roots.

For per-message verification (envelope, gates, revocation enforcement) see model.md. This page covers key issuance, trust roots, and rotation.

What identity rests on (token-only)

Identity is the Directory's Ed25519 signature over a token — there is no per-app X.509 cert binding. Two token protos, both Directory-signed, both verified against the cached Directory public-key set (common/proto/server/directory.proto):

  • IdentityToken (operator + device) — rides inside every AuthEnvelope. Carries principal_id (UUID), roles (with a kind:operator|kind:device prefix), callsign (deprecated — still issued by the Directory, no client reads it for tactical labelling; every tactical callsign is now resolved from the ORBAT projection, see operational-tactical-flow.md), max_classification, key_epoch, batch_id, issued_at_ms, expires_at_ms, directory_key_id, visibility_radius_km, and crucially principal_sign_key (the 32-byte Ed25519 public half used to verify each message's device_signature) and device_id (stable per-device anchor). Verified by verify_identity_token.
  • ServerToken (server listeners) — does not ride AuthEnvelope. Pins hostname, max_classification, coverage_cells, roles. Verified by verify_server_token.

Neither token carries bound_cert_serial — that field is reserved/removed (directory.proto:36,89). Classification rides on the token.

Trust roots

Two independent Directory-held trust anchors:

  1. Directory Ed25519 signing key(s) — sign every IdentityToken, ServerToken, and RevocationList. Served at GET /.well-known/directory-key as DirectoryKeyResponse { public_keys[], issued_at_ms }. Exactly one key is live (retired_at IS NULL); rotation marks the current key retired and inserts a fresh one, and the endpoint serves the current key plus every non-compromised prior key within the overlap window so in-flight tokens still verify (directory signing_key_service). Each token names its signer via directory_key_id. Clients refresh on a cadence (Android: DirectoryKeysRefresher, hourly).
  2. Directory Root CA — anchors transport TLS only (server-auth). Pinned cold-start via ProfileBundle.directory_root_ca_der, re-fetched from the Directory's root-CA well-known endpoint; during root rotation it returns new ‖ previous for ~max_cert_TTL + 1d. This validates the router's server certificate, not client identity. The same CA also anchors server↔server / router federation, the one place a client-presented certificate carries identity: each peer presents a CA-signed leaf whose CN is its Directory-issued principal_id, and the link-layer mTLS verifier pins that CN (server/src/active_peers.rs). That is infrastructure-peer transport identity, not per-app principal identity — app identity stays token-only.

Machine ingest credentials are a separate symmetric trust root

The machine ingest API (/api/feed/*) does not use the Directory Ed25519 key. Its credentials are HS256 JWTs signed with the deployment secret WAYPOINT_API_JWT_SECRET — a symmetric trust root unrelated to either anchor above. Their revocation is independent too: a DB live-check on api_credentials.revoked_at, not the Directory RevocationList. This does not violate token-only identity — it is machine-API auth (authorizing a producer to write feed data), not principal identity, so it carries no clearance and rides no AuthEnvelope. Full contract: protocol/ingest-api.md.

Key material a device holds

  • Device Ed25519 signing key — private half of principal_sign_key. The Directory generates the keypair per token batch, embeds the public half in the token, and returns the private half in the enrollment HTTPS response. It is ephemeral (rotates every login/batch) and is not a long-term device identity. Held in process memory client-side (Android: SecretStore.signWithDeviceKey).
  • Group key — issued by the Directory (GET /api/group-key, Bearer auth; current + previous bundle; key_epoch stamped on the token). Issued to clients only — the Directory denies the key to relay/server principals. The client seals outbound member content (AES-256-GCM) and opens inbound SealedContent with this key. It is not part of envelope authenticity (that is the device signature) — it is the content confidentiality concern. See security/model.md.

Token issuance lifecycle

All issuance flows through the Directory's auth endpoints (directory/start/routes.ts):

Endpoint Auth Issues
POST /api/auth/login FIDO assertion (operators) or single-use registration token (devices) IdentityToken batch
POST /api/auth/extend a still-valid IdentityToken from the current batch refreshed IdentityToken batch
POST /api/auth/device-enroll MDM seed token (scope=enroll-only) device IdentityToken batch
POST /api/auth/revoke admin, or operator self (own only) appends to the revocation snapshot

A server's ServerToken is not issued by a dedicated API endpoint. It's minted in the admin device-creation flow (a server/gateway-platform device with a hostname), carrying the classification ceiling and validity entered on the form, and is delivered to clients embedded in the login response. See Add a server.

Batching + offline. Operators/devices receive a 5 × 7-day IdentityToken batch; servers get a single ServerToken valid for the per-device validity set at creation (1–365 days, default 30). Batching lets token rotation happen offline: each next batch token is sealed and unlocked by a local FIDO presence proof against the cached FIDO public key (operators), so a device survives ~30–35d of Directory unavailability before total credential loss.

Revocation

RevocationList is Directory Ed25519-signed, monotonic by sequence, and carries two levels (directory.proto):

  • revoked_principals — cascades to every token ever issued for a principal_id (operator, device, or server).
  • devices (RevokedDevice) — revokes one device by its Ed25519 device_sign_key.

There is no revoked_certs; identity is token-only.

Distribution: - Bootstrap — signed snapshot over HTTPS at GET /api/revoked-principals, fetched at login and cold-start; cold-start principals also arrive in the login response. - Runtime — a single server router HTTPS-polls the Directory (~30 s) and re-publishes the signed payload verbatim on the Zenoh topic waypoint/global/sec/revocations; native clients subscribe. The relay does not re-sign; verifiers check the Directory signature regardless of transport (RevocationCache.verifySignature / merge).

Enforcement (see model.md): receivers drop on principal or device sign-key after envelope verify; the server force-closes affected sessions on a revoked-list update.

Superseded source docs

  • common/PKI.md — replaced by a link to this page.
  • infrastructure/pki/README.md — its "Production model" section links here; its generate.sh staging helper section stays repo-local (demo/dev VM self-signed TLS, not part of the production trust chain).