Skip to content

Reference source · 0.1.0-dev.11

credential-lifecycle.md

docs/credential-lifecycle.md · Download original file

# Credential authorities, status and requester rotation

This describes the executable experimental trust profile in `0.1.0-dev.3`. Each participant independently admits authorities and chooses their authority limits. Convey supplies no required root, registration service, or runtime directory. The format borrows PKI concepts but is a custom ML-DSA profile, not an X.509 certificate chain or a complete public vendor-enrollment system. [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280.html) remains a reference for the broader issuance and trust model.

## Explicit authority boundaries

`TrustStore(policies, path)` accepts policies keyed by issuer. Each specifies `publicKey`, allowed `kinds`, `subjectPrefixes`, permitted `roles` by kind, `maximumLifetime`, and `maximumStatusAge`. The prototype requires namespace prefixes ending in `:` to avoid accidental partial-prefix matches. Its synthetic identities use URNs. A future public profile must define organization/deployment identifiers and domain or namespace control evidence; the demo namespace is not such evidence.

The development Host authority can issue only `host` identities under `urn:oprp:demo:`. The application/person authority can issue only `application` and `person` identities there, with an explicitly allowed application role. A correctly signed credential outside those limits is rejected. A configured role is a permitted authority assertion, not a patient permission. Emergency eligibility remains a Host-policy decision.

Credential verification checks signature, namespace/kind/roles, validity and maximum lifetime, both key fingerprints, current issuer status, and expected identity/kind. Revocation and disabled-issuer state are persisted when a database path is supplied. Host and SDK operators own their trust databases and private-key stores. The reference Host does not close a caller-owned `TrustStore`; the caller must close it separately.

## Authenticated status and bounded freshness

A signed status document contains `payload` and `signature`. The compact payload contains exactly:

```text
issuer, sequence, issued_at, next_update,
revoked_serials, revoked_signing_kids, revoked_encryption_kids
```

The authority signs ASCII `OPRP-STATUS-0.1.` followed by the encoded payload using ML-DSA-65. `ingestStatus(document, now)` verifies the signature, admitted issuer, bounded validity, nonnegative safe integer sequence/times, and bounded unique revocation lists. The status must already be effective and not have reached `next_update`. Status lifetime cannot exceed that issuer's configured maximum; the demonstration uses 600 seconds.

Each trust database durably stores the highest accepted sequence and signed document. Lower sequences fail. A higher sequence cannot regress issue time. The same sequence is accepted only for the identical encoded payload; a randomized signature over that same payload is harmless. A different payload at the same sequence is equivocation and fails. All listed revocations are added to a durable deny set. Omitting an old revocation from a later document cannot reactivate it.

Serial revocation stops that credential. Signing-key or encryption-key revocation also stops later credentials reusing that key. Local `revoke(issuer, kind, value)` and `disableIssuer(issuer)` are trusted operator controls, not requester operations. Issuer disablement persists and overrides all credentials and historical archive evidence under that issuer. There is deliberately no automatic unrevocation API.

Missing or expired status fails closed before payload decryption. Operators must obtain and ingest updates from their chosen authority through a bounded, authenticated status worker; no public status-download endpoint, worker, trust-list distribution or CA recovery service is implemented yet. The synthetic fixture signs fresh status when its test clock advances. That is test infrastructure, not an online availability strategy. Revocation can be unknown until the next accepted update; freshness bounds that interval rather than eliminating it.

## Renewal versus key rotation

Renewal with the same identity and both keys creates a new serial/validity interval. It does not create new patient authority or extend the grant. The renewal test revokes the original serial, replaces the SDK credential with a newly issued same-key credential, and successfully reads and refreshes under the original bounded grant. A revoked key cannot be repaired by issuing another serial. Private-key loading, renewal automation, issuer-key transitions and lost-key recovery remain operational work.

Planned requester rotation changes both signing and encryption keys. It requires current credentials and proof of control of both the old and new signing keys. `makeRotationProof` creates a payload binding `rotation_id`, destination `host`, requester `subject`, both old/new key IDs, `issued_at` and `expires_at`. Both identities sign ASCII `OPRP-ROTATION-0.1.` plus payload. The proof lives at most 120 seconds and is transmitted in a `rotate_requester` exchange signed and encrypted using the new identity.

Each Host pins a requester's current key pair durably. A second credential for the same subject cannot acquire new preauthorized grants, adopt pending requests, or use old grants merely by presenting a new key. It must complete rotation. When upgrading older prototype storage, existing active grants constrain the first accepted signing key.

The Host validates both credentials/statuses, subject, Host, time, signatures, old pin and new caller. One transaction rebinds the requester's grants and authorization requests, replaces the current pin, and retires old refresh credentials/recovery receipts. Grant scopes, original end times, actor context and active/revoked state remain unchanged. Revoked or expired grants stay unusable.

The rotation has a durable receipt keyed by requester and rotation ID. Retrying the identical proof using the still-current new keys can recover the receipt after Host restart or old-proof expiry. That retry does not execute rotation again and does not require the now-retired old credential to remain valid. Altered content or a superseded new key fails. Initially expired proofs and proofs without both signatures fail.

Use `replace_token` with the existing `grant_id` after rotation. It checks current caller, active grant, representative authority and original end time, then issues a short JWT under the current Host key. A refresh credential is issued only if that grant already permitted refresh. The SDK method stores the replacement and clears pending old refresh state. Repeated replacement/polling currently can create additional refresh families; lifecycle refinement and cross-process coordination remain gates. Replacement is an explicit SDK method rather than an automatic lost-key recovery flow.

Each Host's pin is local. Coordinate planned rotation with every relevant Host and retire old authority credentials/keys when the transition finishes. No automatic cross-Host broadcast exists. Compromise or loss of the old private key needs a separately authenticated recovery or new-authorization process; possession of a new CA credential alone cannot bypass an existing pin. Host-key rotation, overlapping receiver-key retention and address-authority rotation are separate unfinished profiles.

## Offline archive evidence

Archives carry their source Host credential and its signed status at creation. Verification evaluates those historical documents at `created_at` under independently pinned authority policies, while still honoring all locally known revocations and disabled issuers. Historical evidence is never ingested into the live cache and cannot roll its sequence back. Current status need not be fetched to read an offline backup.

A signed creation time is not an independently notarized timestamp. An offline reader cannot know later compromise or revocation without newer trust information, and known compromise makes the default verifier fail closed. Long-term evidence, timestamping, compromise adjudication and patient recovery exceptions need a reviewed profile. See [portability](portability.md).

## Verification and independent implementation

The [lifecycle tests](../tests/credential-lifecycle.test.ts) cover issuer boundaries, signed-status forgery/staleness/rollback/equivocation, durable revocation and issuer disablement, renewal, dual-key rotation, retired refresh credentials, unchanged scopes/end times, restart and retry. The independent Python codec verifies supplied authority policies and signed fresh status using PQClean, but does not implement a durable status ledger or rotation service. It remains a cryptographic interoperability harness, not a conformant full SDK. [Structural schema](../schemas/trust.schema.json).