Add public certificate derivation service #383
Labels
No labels
Compat/Breaking
Component/Enclave-Builder
Component/Infrastructure
Deploy/BYOC
Deploy/Full-managed
Deploy/Self-hosted
Interface/API
Interface/CLI
Interface/Git
Interface/Web
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority/Critical
Priority/High
Priority/Low
Priority/Medium
Reviewed/Confirmed
Reviewed/Duplicate
Reviewed/Invalid
Reviewed/Won't Fix
Status/Abandoned
Status/Blocked
Status/Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
public/platform#383
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Current status — 2026-09-24
Core certificate/key-service implementation is complete; service security and lifecycle acceptance remain open. This supersedes older claims that authentication, readiness, critical-notation checks or fixed fixtures are unimplemented/unpublished.
Published PR heads checked on 2026-09-24: Platform
1f7e296, Locksmith9f702bb, Bootproof821b5c6. Platform's dependency/model/mock/default-runtime pins are aligned at Locksmithd2876e9; subsequent Locksmith changes concern examples, configuration, documentation and terminology. These are published implementation states, not merged/deployed-state claims.Implemented: deterministic public-certificate derivation, server-generated bundle IDs, CA-certified organization/bundle/index, proof verification, critical hashed notations, fixed V1 encoding fixtures, external-PGP root bootstrap, pre-listen root/CA validation, cached/coalesced readiness, bearer-authenticated issuance, and worker limits that remain held through actual completion. Platform pins already include the verifier changes. See V1 contract and service hardening.
Remaining:
The September 23 assessment ran 11 default release tests, all 19 certificate-service library cases (the socket-dependent cases passed after a sandbox-permission rerun), nine mocked Nitro lifecycle/bootstrap tests and four restart-configuration tests. Packaging validation also passed. These results belong to Platform
44a475d/ Locksmithc33e35e; the later terminology updates are not a new acceptance run. Existing operator-reported successful recovery should be retained and attached to exact revisions/PCRs/scenarios. A consolidated current-candidate adversarial, hardware and real-Nitro restart matrix is still not recorded in the tracker.Accepted boundary: an HTTP-process restart preserves Keyfork's root and invalidates pending approvals; an enclave restart requires external-PGP recovery of the existing root. The accepted CA-certified UID/context profile and frozen V1 encodings remain unchanged; the general structure-hash registry is deferred. See the current deployment runbook.
Current implemented contract
The public-certificate models define:
PublicCertificateRequest::V1withorganization_id: [u8; 16]andcertificate_count: NonZeroU8. The caller cannot supplybundle_id, paths or indices; the route generates a fresh v4 UUID.PublicCertificateResponse = Proofed<PublicCertificateBundle>, serialized as{data, necroproof}. V1datacontains canonical 16-byteorganization_id/bundle_idandcertificates: Vec<String>.Caution public certificate index=N; a standard embedded Caution CA UID certification carries criticalorganization-id@caution.coandbundle-id@caution.conotations. Recryption verifies this certification and reads its signed UID index independently of holder ordering.expires_ator response-level expiration field. Preserve current generation-time eligibility and durable-anchor behavior for this first version; new expiration/profile/rotation semantics require an explicit version and migration decision.The accepted current V1 service hash is SHA-256 of the typed versioned CBOR serialization. Published fixed fixtures freeze its bytes; the general structure identifier/encoding registry is explicitly later work.
Goals
Containerfile.public-cert-servicewithout changing the lifecycle of the existing Keymaker image.m/pgp'/kmkr'/...namespace.public-certificate-modelsrequest/response contract.Proofed<PublicCertificateBundle>response with a top-level necroproof overdata.Caution public certificate index=Nand a standard Caution CA certification over that UID with exactly one critical, hashed organization-ID notation and one critical, hashed bundle-ID notation.Non-goals
/generate_quorumroute, Keymaker V1 necroproof changes, or the Shard Recryption Service transport, although it defines the proofed artifact those systems will consume.Trust model
The encrypted Caution team quorum bundle is part of the service configuration. It contains the public OpenPGP certificate corresponding to its root secret but does not expose that secret.
On every enclave boot:
keyforkd.m/pgp'/0'unless the team chooses a different configured signer path.The fingerprint comparison prevents the service from becoming ready with the wrong root, wrong bundle, or wrong signer configuration. Consumers must trust a separately configured copy of the Caution team public certificate or fingerprint; a public certificate returned by the service must never become its own trust anchor.
Process lifecycle: implemented behavior and retained requirements
Platform's vault startup runs Locksmith/Keyfork recovery before the application unit starts. The encrypted
PUBLIC_CERTIFICATE_SERVICE_TOKENreplaces the old bootstrap marker; the start wrapper then execs the Rust HTTP service. The implemented deployment does not embed quorum recovery inside one new supervised Rust process.The custody root belongs to the enclave lifetime. HTTP-process restart preserves Keyfork's recovered root, loses pending approvals and rechecks readiness before serving. Full enclave restart requires fresh external-PGP quorum recovery of the same root. Root/CA mismatch prevents startup; unusable Keyfork fails operations closed.
/healthreturns 503 for unavailable/mismatched root state, with a two-second cache and single refresh. It does not expose a four-state recovery-progress API or install a background monitor.Original lifecycle goals remain to be reconciled before closure: explicit locked/reconstituting/ready/failed reporting, reusable non-daemonizing recovery integration, and supervision/termination behavior. The accepted enclave-lifetime restart decision supersedes only the old requirement to re-quorum after an HTTP-process restart; this status update does not silently waive the other goals. The service must never serve derivation before successful root recovery/validation or expose sensitive recovery progress.
Request behavior
POST /v1/public-certificatesaccepts only the current V1 request. Platform authorizes the organization; the service generates the bundle UUID and consecutive certificate indices0..certificate_count. No caller-selected path or index is accepted. Full organization/bundle identities and index context must be authenticated despite lossy path encoding.The bearer check runs before request-body processing. Request-body/count limits, one generation worker and a 60-second budget bound derivation work. Overload/unavailable service returns 503. The profile below is accepted for the first test; shipping the local stricter verification still requires publication and aligned downstream pins.
Certificate derivation
The path algorithm and key ordering are implemented. The profile below records the accepted current CA-certified UID binding; it replaces the original proposal to put all context in critical self-notations.
Base path
The base path is:
m/pgp'is the existing Keyfork OpenPGP namespace. Thekmkrcomponent is constructed with:and converted to a hardened
DerivationIndex.UUID conversion
For both the organization ID and bundle ID:
u32.word & 0x7fff_ffff.DerivationIndex.For certificate index
i, the complete account path is:The highest-bit removal is intentionally lossy. Consequently:
OpenPGP certificate profile
The certificate profile is part of the protocol and must be versioned. Subkey ordering matters because
keyfork-derive-openpgpderives each key at a child index below the certificate account path.For compatibility with the current Keymaker validation and derivation conventions, the initial profile should use the existing order:
For each certificate:
keyforkd.Caution public certificate index=N.organization-id@caution.coand one hashed, criticalbundle-id@caution.conotation with full UUIDs emitted as lowercase hex.PublicCertificateBundle::V1.certificates.The underlying keys depend on the root and path. Fixed fixture certificate bytes are compatibility examples, not a promise that signatures from repeated issuance at another time are byte-identical. Preserve existing lifetime behavior; the current schema has no caller-controlled expiration field.
UID binding — accepted current V1 profile
The trusted CA certification binds the canonical UID index and both full UUID notations. API and recryptor must verify the expected CA, signature and authenticated context, rejecting missing, duplicate, unhashed, noncritical or mismatched required notations. The recryptor also rejects conflicting certified contexts. The API checks issuance order; recovery uses the certified index independently of later mixed-holder position. This is the accepted replacement for the original critical-self-notation design.
Necroproof and structure hashing
The response is
Proofed<PublicCertificateBundle>: versioneddataplus a separatenecroproof. Current V1 uses:serde_cbor::to_vec(&PublicCertificateBundle); Nitrouser_datamust match, and nonce must be absent/null.serde_cbor::value::to_value(&GenerateQuorumBundle); Nitrouser_datamust match. Its nonce is SHA-256 of CBOR serialization of("keymaker-generate-quorum-necroproof-nonce-v1", bundle_hash.as_slice()).Keep these distinct byte constructions unchanged. The full UUIDs, complete certificate list and ordered associations are hash-bound. Current separation uses strict typed schemas, different required payloads/hashes, nonce expectations and configured PCR trust; the public-certificate hash has no extra global structure identifier. Fixed fixtures exercise each construction and reject cross-artifact decoding/mismatched payload hashes.
Explicit deferral: a general Caution-wide structure identifier/hash registry is later work, replacing its former pre-artifact prerequisite for this first version. Any incompatible encoding/profile/required-field change needs an explicit version and artifact-migration decision. Future artifact types must not assume V1's existing separation covers them automatically.
Repository implementation and remaining lifecycle integration
crates/public-certificate-modelsandcrates/public-cert-serviceexist. Service code covers HTTP routes, derivation, root checks, proof generation, issuance admission and release integration; tests cover these boundaries. The executable uses the zeroizing allocator. Recryption shares the derivation contract and verifies certified context rather than accepting arbitrary HTTP paths.The deployed example uses Platform-managed Locksmith/Keyfork bootstrap. The original proposal to extract reusable non-daemonizing recovery/startup functions and integrate them with a supervised service remains a lifecycle reconciliation item; it is not claimed as implemented by the existing start wrapper. Any such integration must keep the existing
locksmithdworking, use structured errors and preserve Locksmith's ownership of Keyfork startup.Keep path construction independently testable, with canonical UUID/path vectors and identical derivation semantics in issuance and recovery. See the derivation source.
Container packaging and production requirements
The deployable combined certificate/recryptor example uses examples/certificate-service/Containerfile, its
caution.hcl, start wrapper and input preflight. It packages the existing root bundle, root Keymaker policy, encrypted issuance token, public CA and release trust configuration. The older root-level Containerfile proposal is not the current deployment recipe.Reproducible pinned StageX builds, locked/offline compilation, correct runtime artifacts, no plaintext root/test-private-key material, pre-recovery unavailability and clean termination remain acceptance requirements. Do not infer final-candidate build/PCR evidence from an older build-stage digest.
For this first test, retain the existing root bundle/CA and verified build inputs using the operator runbook. Named custodians, formal production ownership, organizational recovery drills and long-term update/rotation procedures are later work; this does not relax artifact integrity or enclave/quorum boundaries. Preserve the existing root/CA during the token/service upgrade. The example's Keyfork socket is
/keyforkd.sock, release configuration is/etc/caution/release-config.json, and HTTP listens on 8080; production listener exposure still needs to satisfy #384 while allowing public WebAuthn-authorized release. See the migration and acceptance runbook.Security requirements
dataand compute it over the exact current V1 typed-CBOR bytes; do not place an inline V0-style necroproof inside the payload.Tests
Unit tests
certificatesvector is complete, includes only armored public certificate strings, and does not include separate derivation paths, certificate fingerprints, signer fingerprints, index fields, or CA signature fields.data, is not embedded inline in the payload, and changes whenever any proofed bundle data changes.Lifecycle and integration tests
certificate_countpublic certificates for indices0..count.Container tests
Remaining implementation and acceptance sequence
Acceptance criteria
The service is ready for integration when:
Proofed<PublicCertificateBundle>, valid necroproof over the exact V1databytes, and certificates with canonical self-signed index UIDs plus valid Caution CA certifications carrying critical, hashed organization/bundle notations.First-test scope and later production work
m/pgp'/0'root CA path are implemented; they are not open design questions. Basic root integrity, quorum recovery and enclave-only private material remain required now.Relationship to organization-user quorum bundle creation
This issue should stay separate from https://codeberg.org/caution/platform/issues/384 rather than being merged into it.
#383 tracks the public certificate derivation service and its proof/certificate contract: Locksmith/Keyfork recovery, deterministic OpenPGP certificate derivation, embedded UID bindings, top-level necroproofs, independent verification, and the dedicated service container.
#384 tracks the platform product/API/CLI/dashboard flow that consumes this service: selecting organization users, enforcing explicit Caution-backed key selection, storing generated quorum bundles, calling Keymaker, and presenting the workflow to users.
Treat #383 as an implementation prerequisite for the Caution-backed/WebAuthn-derived certificate path in #384. #384 can still make progress on discovery, authorization, storage, UI scaffolding, and existing OpenPGP-user paths, but it should not close until the #383 service contract is implemented and integrated.
Hmmm what do we do about expiration for these certs that are derived by the public certificates service? @ryansquared
keyforkd is started automatically by locksmith and does not require special handling. once locksmith is done, the server will be ready to go without issue.
Drop the derivation_path and fingerprint from the returned certificates, it's implicitly trusted information that we don't want. That information should be derived on command.
Signer fingerprint is automatically in the hashed area of the signature, drop it from the response as well.
At this point in time, we don't have any canonical signature across anything we do. This includes manifests (we only sign some arbitrary hash) and whatever we might use eventually. We should come up with a standard format and identifier registry for hashing versioned structures deterministically.
We should have the CA sign every derived certificate, as it will be required on the verifier side for the Shard Recryption Service to validate the derived path produces the correct, signed, cert.
We're missing a necroproof on the generated bundle. The response should be
{"data": "whatever", "necroproof": hash_over_data()}. Note: v0 bundles included the necroproof inline. This should be changed.Updated the issue body to incorporate the review comments:
keyforkd.derivation_path, certificatefingerprint, andsigner_fingerprintfrom the response contract; verifiers recompute paths/fingerprints and rely on OpenPGP signature metadata plus configured trust anchors.In short, we should basically be replacing "signature" with "necroproof".
Caught the two review comments that arrived just before my previous update and revised the design again:
dataenvelope plus a separatenecroproofoverdata, explicitly avoiding the old v0 inline-necroproof layout.Audited the full comment thread and made one final correction for the later necroproof guidance:
signaturefield is now removed entirely; the bundle-level authenticity check is the top-levelnecroproofoverdata.I rechecked the issue body for all requested markers:
expires_at, Locksmith-owned Keyfork startup, omittedderivation_path/fingerprint/signer_fingerprintfields, shared structure-hash registry, per-certificateca_signature, top-leveldata/necroproof, and no remaining bundle-level"signature"response field.The CA signature should be a UID signature in the standard OpenPGP format, placed upon the derived certificate itself.
Updated the design for the latest OpenPGP-format clarification:
ca_signatureresponse field from certificate entries.dataenvelope.Updated after the public certificate contract branch merged into
caution/locksmithmain(12d5f17).Key corrections:
public-certificate-modelsnow exists, so #383 no longer asks to create that model crate.[u8; 16]canonical UUID bytes fororganization_idandbundle_id.organization_id,bundle_id, andcertificate_count; it does not includeexpires_at.Proofed<PublicCertificateBundle>with top-leveldata+necroproof.organization_id,bundle_id, andcertificates: Vec<String>; certificate indices are embedded as certificate self-notations, not response-level fields.Updated the certificate binding language from the latest Control Tower notes:
Caution Key-style UID.This matches the design note that Keymaker binds associations via necroproof, while the in-certificate UID signatures bind each cert to the org/bundle/index context.
Opened the first Locksmith-side implementation PR for this issue:
This is intentionally a foundation PR rather than a full close: it adds the dedicated
public-cert-servicecrate/binary, deterministic public-certificate derivation path helpers and test vectors, locked-state HTTP boundary, fail-closed derivation endpoint, and a separate service Containerfile. Follow-up PRs still need to wire Locksmith/Keyfork recovery, derive and sign OpenPGP certificates, compute/verify necroproofs, and implement independent certificate verification.Certificate derivation, proof/CA verification and external-PGP custody bootstrap are integrated into the demonstrated end-to-end flow.
Outstanding requirements in the current issue:
These are service-contract gaps, not evidence of unauthorized share release. Quorum bootstrap itself is implemented through Platform’s vault startup.
Reconcile older contract wording with the later implemented design: server-generated bundle UUIDs, CA-certified organization/bundle notations and certificate index in signed UID text. Define the intended restart boundary and production root ownership/update procedure before closing.