feat(pki-server): authorize signed OCSP requests

Add per-responder signed-request policies, strict request-signature and
requester-certificate validation, cryptographic principal mapping and
scoped OCSP query authorization.

Preserve public unsigned responder behavior while isolating requester,
transport and administrative identities.
This commit is contained in:
2026-08-05 19:06:13 +02:00
parent 44cbb0a37d
commit 4f01c57360
17 changed files with 1468 additions and 87 deletions

View File

@@ -12,7 +12,9 @@ An OCSP responder is a durable server-control record. Its alias binds exactly
one realm, logical authority, issuer generation, responder certificate,
`KeyRef`, explicit chain path, signature algorithm/binding commitment, nonce
policy, response-validity policy, accepted CertID hashes, and finite request
bounds. Activation revalidates those dependencies and freezes their
bounds. Each record also carries an explicit request-authentication mode and,
for signed modes, a finite immutable requester trust, mapping, signature,
revocation, and chain-bound policy. Activation revalidates those dependencies and freezes their
commitments. There is no first/last issuer selection, filename inference,
timestamp inference, runtime path building, or fallback signing algorithm.
@@ -66,6 +68,77 @@ nonce echo:
nonce-bearing responses use no-store
```
## Request authentication
Every responder selects exactly one mode:
- `UNSIGNED_ONLY` is the default public responder policy. Unsigned requests are
accepted and syntactically signed requests receive OCSP `unauthorized`.
- `UNSIGNED_OR_AUTHORIZED_SIGNED` preserves anonymous requests, but any signed
request must authenticate and authorize completely; it never falls back to
anonymous handling.
- `AUTHORIZED_SIGNED_REQUIRED` returns OCSP `sigRequired` for unsigned requests
and accepts only authorized signed requests.
Signed request authentication is protocol-scoped. ZeroEcho retains the exact
canonical `tbsRequest` DER, requires a canonical `directoryName`
`requestorName` equal to the signer certificate subject, validates the bounded
included signer chain against configured trust selectors, and verifies the
signature using an active `OCSP_REQUEST_SIGNATURE` binding. Current standard
bindings permit ECDSA/SHA-256, Ed25519, RSA PKCS#1/SHA-256 and strict
RSA-PSS/SHA-256 when explicitly listed. SHA-1 request signatures are rejected.
Trust and authorization are independent. A cryptographic certificate, SPKI, or
managed issuer-generation-plus-serial commitment must map to exactly one enabled
persisted `SecurityPrincipal`. That principal needs an explicit
`OCSP_STATUS_QUERY` grant scoped to the exact realm, responder, authority, and
issuer generation. Explicit deny wins. `OCSP_ADMINISTER` grants no query right,
and query authority grants no administration, content, search, ownership,
disclosure, or PII right. Multi-entry authorization is all-or-nothing.
Requester certificate revocation uses one explicit policy:
- `LOCAL_AUTHORITATIVE_REQUIRED` requires a managed ZeroEcho credential and
current local revocation state. It is the recommended closed-enterprise mode.
- `LOCAL_AUTHORITATIVE_IF_MANAGED` checks managed credentials locally and uses
validity plus configured trust for external credentials.
- `VALIDITY_AND_TRUST_ONLY` performs no revocation check and must be selected
deliberately with its stale-credential risk understood.
ZeroEcho does not follow AIA, CRL, or OCSP URLs while authenticating a requester
and never calls the responder recursively. A request signature authenticates
integrity and requester identity; it does not replace nonce freshness. Replay of
a permitted read-only request remains valid when nonce policy permits it.
`REQUIRED` nonce responders keep a finite derived process-local replay window;
restart may discard that operational window without changing PKI authority.
Safe policy examples omit real identities and commitments:
```text
public unsigned:
requestAuthenticationMode = UNSIGNED_ONLY
mixed transition:
requestAuthenticationMode = UNSIGNED_OR_AUTHORIZED_SIGNED
requesterRevocationPolicy = LOCAL_AUTHORITATIVE_REQUIRED
closed managed enterprise:
requestAuthenticationMode = AUTHORIZED_SIGNED_REQUIRED
trustSelectors = [managed issuer generations]
requiredPermission = OCSP_STATUS_QUERY
requesterRevocationPolicy = LOCAL_AUTHORITATIVE_REQUIRED
closed external trust:
requestAuthenticationMode = AUTHORIZED_SIGNED_REQUIRED
trustSelectors = [configured trust anchors]
requesterRevocationPolicy = LOCAL_AUTHORITATIVE_IF_MANAGED
```
Migration is explicit: replace the pre-release responder record with its desired
mode and complete requester policy, validate scoped grants, and activate it
through the configured approval workflow. The obsolete schema is rejected; no
mode is inferred and no compatibility reader exists.
## Requests and responses
POST uses `Content-Type: application/ocsp-request`. GET uses an unpadded,
@@ -76,10 +149,10 @@ canonical URL-safe Base64 request path:
```
Query-string requests and alternate Base64 normalization are rejected. The
strict DER parser accepts unsigned requests only, consumes the complete input,
rejects duplicate or unknown critical extensions, and enforces configured byte
and entry bounds. Signed OCSP requests are rejected in this release; a request
signature is never treated as administrative or certificate-owner authority.
strict DER parser consumes the complete input, rejects all per-entry extensions
and duplicate or unknown critical request extensions, and enforces configured
byte, entry, signer-certificate, and chain bounds. Signed requests additionally
require the exact canonical optional-signature structure described above.
Configured CertID hashing supports SHA-1 and SHA-256. SHA-1 is permitted only
for RFC-compatible issuer-name/key hashes. It is never enabled as a certificate
@@ -98,18 +171,22 @@ recovery-required authority returns a transient protocol/transport failure, not
Successful responses are strict signed DER with
`application/ocsp-response`, `nosniff`, `producedAt`, `thisUpdate`, and
`nextUpdate`. Nonce-free responses carry a strong ETag and bounded public cache
`nextUpdate`. Nonce-free unsigned responses carry a strong ETag and bounded public cache
policy. An echoed nonce is returned exactly and makes the response non-cacheable.
Every authenticated signed response uses `Cache-Control: private, no-store` and
`Pragma: no-cache`, even without a nonce; shared caches never store it.
No request DER, response DER, serial, issuer hash, nonce, certificate identity,
key reference, or parser/provider exception is logged or audited.
## Deployment topology and capacity
Direct mode permits anonymous OCSP over the public TLS listener. An optional
public client certificate does not change certificate status, and forwarded
identity headers are rejected. In trusted-reverse-proxy mode the backend proxy
hop still requires mTLS and the dedicated forwarding permission; proxy and
forwarded end-client identities do not authorize or select OCSP status. Source
public client certificate neither replaces nor supplements the signed requester
and does not change certificate status; forwarded identity headers remain
rejected. In trusted-reverse-proxy mode the backend proxy hop still requires
mTLS and the dedicated forwarding permission. The proxy transport principal,
forwarded administrative principal, and signed OCSP requester remain distinct
and do not inherit one another's permissions or select OCSP status. Source
IP, `Forwarded`, and `X-Forwarded-For` are never responder authority.
OCSP has a separate bounded runtime lane even though it shares the public