Files
ZeroEcho/docs/pki-server-public-repository.md
Leo Galambos c3bd3a33e9 feat(pki-server): add public PKI repository API
Add the disclosure-controlled public certificate, chain, CRL and status
repository with capability-based unlisted access, bounded streaming,
conditional caching and isolated public execution resources.

Introduce authoritative issuer generations and explicit chain paths so
issuance bundles and stable public chain routes never rely on inferred
certificate ordering or runtime path guessing.
2026-08-05 01:48:52 +02:00

4.6 KiB

ZeroEcho public PKI repository

The public repository is an optional listener that is configured independently from the administrative HTTPS listener. Both listeners use the same realm and long-lived PKI session, but they have separate ports, TLS policy, worker pools, queues and admission limits. Administrative routes are never registered on the public listener, and public routes are never registered on the administrative listener.

Non-loopback public service requires TLS. Explicit loopback plaintext is intended only for deterministic development tests. A direct TLS listener requests an optional client certificate: an absent certificate is anonymous, while a supplied invalid or unmapped certificate fails authentication. A trusted-reverse-proxy listener requires mutual TLS on the proxy-to-ZeroEcho hop. Its transport principal must have only FORWARD_AUTHENTICATED_CLIENT_IDENTITY; an absent forwarded client identity remains anonymous, and an invalid forwarded identity never falls back to anonymous. RFC 9440 is preferred. The explicitly selected NGINX escaped-PEM format uses the same strict validation described in the administrative-server guide.

Disclosure and retrieval

PUBLIC objects are anonymously retrievable. AUTHENTICATED, OWNER_ONLY and RESTRICTED objects require the corresponding end-client identity and scope. NOT_PUBLISHED objects are unavailable. PUBLIC_UNLISTED retrieval uses exactly:

Authorization: Bearer <one-time-issued-capability>

Capabilities are never accepted in a query, cookie, form body or alternate header. They are bound to one realm, object, action and expiry. Capability responses are private and non-cacheable. An object ID is not authentication authority, and the repository intentionally provides no leaf-certificate search or listing endpoint.

Single certificates and CRLs use canonical DER with application/pkix-cert and application/pkix-crl. Explicit chain paths and the stable chain route return deterministic JSON containing the ordered certificate retrieval links and the path commitment; no ambiguous concatenated DER representation is defined.

Immutable public objects use strong ETags and the configured immutable cache duration. Stable alias routes use both alias and target commitments and a shorter configured cache duration. Authenticated responses are private and non-cacheable. HEAD performs the same authorization and integrity checks as GET, and If-None-Match returns 304 only after access has been re-evaluated.

The versioned route set is deliberately finite:

GET|HEAD /public/v1/authorities
GET|HEAD /public/v1/authorities/{authorityId}
GET|HEAD /public/v1/authorities/{authorityId}/issuers/{issuerId}/certificate
GET|HEAD /public/v1/authorities/{authorityId}/issuers/{issuerId}/paths/{pathId}
GET|HEAD /public/v1/authorities/{authorityId}/chain
GET|HEAD /public/v1/authorities/{authorityId}/crl
GET|HEAD /public/v1/certificates/{credentialId}
GET|HEAD /public/v1/status/{statusObjectId}

Authority listing uses bounded keyset pagination through after and limit. No leaf-certificate collection or search route exists. Explicit chain paths are never inferred from certificate dates, filenames, local trust stores, or list order.

Stable repository aliases

Issuer generations and chain paths are PKI authority. Repository aliases are separate operational metadata. CURRENT_CRL points to one exact public CRL status object. CURRENT_CHAIN points to one exact issuer generation and immutable chain path. Updates require expected-current conflict protection and never alter the target object, issuance selection, revocation state or disclosure policy.

A typical administrative workflow is:

status.generate
→ inspect the status object
→ security.disclosure.set to PUBLIC
→ repository.alias.set with type CURRENT_CRL and expected-current commitment
→ GET /public/v1/authorities/{authorityId}/crl

Chain publication is similarly explicit: validate or register the backend chain path, make every path certificate public, then set CURRENT_CHAIN. Generating a CRL or rotating an issuer never changes a public alias automatically.

The direct public-listener example is in docs/pki-server-loopback-example.json. The RFC 9440 trusted-proxy example is in docs/pki-server-trusted-proxy-rfc9440-example.json. The production and NGINX administrative examples explicitly disable the public listener with "publicListener":{"enabled":false}. A disabled section rejects every ignored listener field; enabling requires the complete independent transport, authentication, capacity, deadline, and cache configuration.