Add public certificate derivation service #383

Open
opened 2026-07-14 21:56:43 +00:00 by caution-bot · 17 comments
caution-bot commented 2026-07-14 21:56:43 +00:00 (Migrated from codeberg.org)

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, Locksmith 9f702bb, Bootproof 821b5c6. Platform's dependency/model/mock/default-runtime pins are aligned at Locksmith d2876e9; 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:

  • Complete actual issuance/network restrictions with #349, while retaining public holder-authorized recryption; review bearer exposure and prove coordinated token rotation.
  • Harden public recovery/release resource admission with Locksmith #12; current worker/body limits do not establish resilience to sustained hostile traffic.
  • Resolve the retained lifecycle/status/supervision criteria against Platform-managed bootstrap; demonstrate HTTP restart, full enclave restart, same-root recovery and wrong-root failure on the intended deployment.
  • Explicitly assess shared-root/private-key capability isolation, production custodians, compromise response and root/credential migration. Existing account deletion is not recryptor revocation. These production concerns remain open even though formal operations were deferred beyond the first test.
  • Triage reachable dependency advisories, finish independent security/fuzz testing, verify the final image/PCRs, and record current acceptance before review/closure.

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 / Locksmith c33e35e; 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:

  • Request: tagged PublicCertificateRequest::V1 with organization_id: [u8; 16] and certificate_count: NonZeroU8. The caller cannot supply bundle_id, paths or indices; the route generates a fresh v4 UUID.
  • Response: PublicCertificateResponse = Proofed<PublicCertificateBundle>, serialized as {data, necroproof}. V1 data contains canonical 16-byte organization_id/bundle_id and certificates: Vec<String>.
  • Certificates are public ASCII armor. Count is derived from the vector; no trusted response-level derivation path, fingerprint, signer fingerprint or index object is returned.
  • The implemented UID is Caution public certificate index=N; a standard embedded Caution CA UID certification carries critical organization-id@caution.co and bundle-id@caution.co notations. Recryption verifies this certification and reads its signed UID index independently of holder ordering.
  • There is no request expires_at or 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.
  • Consumers verify the outer proof, independently configured CA/PCR policy and certified full UUID/index context. Returned values never establish their own trust anchor.

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

  • Build the service in a dedicated Containerfile.public-cert-service without changing the lifecycle of the existing Keymaker image.
  • Require Caution team quorum recovery after every enclave boot or restart; an HTTP-process restart preserves the enclave-owned root and invalidates pending release approvals.
  • Keep the reconstructed root secret and all derived private key material inside the service enclave.
  • Derive certificates only within the prescribed m/pgp'/kmkr'/... namespace.
  • Implement the merged public-certificate-models request/response contract.
  • Return public certificates in a versioned Proofed<PublicCertificateBundle> response with a top-level necroproof over data.
  • Add the self-signed canonical UID Caution public certificate index=N and a standard Caution CA certification over that UID with exactly one critical, hashed organization-ID notation and one critical, hashed bundle-ID notation.
  • Make the result independently verifiable by the Caution API, Keymaker, and Shard Recryption Service.
  • Preserve enough version and key identity information in trusted configuration / certificate metadata to support future root, signer, certificate-profile, and derivation-scheme rotations.

Non-goals

  • The service does not generate or persist a new random root.
  • The service does not store the reconstructed Caution team secret in the image or on disk.
  • The service does not use a separate standalone CA private key.
  • The service does not run in the same runtime as the self-nuking Keymaker service.
  • This work does not implement the Keymaker V1 /generate_quorum route, Keymaker V1 necroproof changes, or the Shard Recryption Service transport, although it defines the proofed artifact those systems will consume.
  • Callers cannot submit arbitrary derivation paths, certificate indices, certificate fingerprints, signer fingerprints, or service-internal routing details.

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:

  1. Locksmith loads the Caution team quorum bundle.
  2. Caution team members submit enough shards to meet its threshold.
  3. Locksmith reconstructs the root secret in memory.
  4. Locksmith initializes keyforkd.
  5. The service asks Keyfork for the standard Caution team OpenPGP account, currently expected to be m/pgp'/0' unless the team chooses a different configured signer path.
  6. The service derives its signer certificate and checks that it matches the configured Caution team public certificate / bundle public key.
  7. The service becomes ready only after that check succeeds.

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_TOKEN replaces 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. /health returns 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-certificates accepts only the current V1 request. Platform authorizes the organization; the service generates the bundle UUID and consecutive certificate indices 0..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'/kmkr'

m/pgp' is the existing Keyfork OpenPGP namespace. The kmkr component is constructed with:

u32::from_be_bytes(*b"kmkr")

and converted to a hardened DerivationIndex.

UUID conversion

For both the organization ID and bundle ID:

  1. Use the UUID's canonical 16-byte representation.
  2. Split the bytes into four consecutive four-byte chunks.
  3. Decode each chunk as a big-endian u32.
  4. Clear its highest bit with word & 0x7fff_ffff.
  5. Convert the result to a hardened DerivationIndex.
  6. Append the four indices in byte order.

For certificate index i, the complete account path is:

m/pgp'/kmkr'/<org-0>'/<org-1>'/<org-2>'/<org-3>'/<bundle-0>'/<bundle-1>'/<bundle-2>'/<bundle-3>'/<i>'

The highest-bit removal is intentionally lossy. Consequently:

  • The necroproofed bundle data must bind the full, unmodified organization and bundle UUIDs.
  • The organization ID must be authorized by the Caution API and the bundle UUID generated by the certificate service, rather than either being freely supplied by an untrusted caller.
  • The Shard Recryption Service must verify the full IDs and bundle context, not merely reconstruct and compare the masked path.

OpenPGP certificate profile

The certificate profile is part of the protocol and must be versioned. Subkey ordering matters because keyfork-derive-openpgp derives 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:

  1. Primary certification key.
  2. Signing subkey.
  3. Transport- and storage-encryption subkey.
  4. Authentication subkey.

For each certificate:

  1. Request the account XPrv for its complete path from keyforkd.
  2. Derive the OpenPGP certificate with the current V1 key order and canonical self-signed UID Caution public certificate index=N.
  3. Add the Caution CA's standard UID certification, containing exactly one hashed, critical organization-id@caution.co and one hashed, critical bundle-id@caution.co notation with full UUIDs emitted as lowercase hex.
  4. Export only public certificate packets; confirm the result parses and retains its self-signed UID and CA-certified context.
  5. Return the ASCII-armored certificate in 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>: versioned data plus a separate necroproof. Current V1 uses:

  • Certificate data: SHA-256 of serde_cbor::to_vec(&PublicCertificateBundle); Nitro user_data must match, and nonce must be absent/null.
  • Keymaker data: SHA-256 of CBOR serialization of serde_cbor::value::to_value(&GenerateQuorumBundle); Nitro user_data must match. Its nonce is SHA-256 of CBOR serialization of ("keymaker-generate-quorum-necroproof-nonce-v1", bundle_hash.as_slice()).
  • Both require independent PCR trust and authenticated generation-time proof verification. Historical provenance never replaces current WebAuthn authorization or destination freshness.

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-models and crates/public-cert-service exist. 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 locksmithd working, 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

  • Use the zeroizing allocator already used by Keymaker, and explicitly zeroize reconstructed secret buffers where practical.
  • Do not persist the reconstructed root or any derived private key.
  • Do not include private OpenPGP packets in responses, traces, errors, or tests.
  • Use the canonical self-signed UID index and CA certification with exactly one hashed, critical organization-ID and bundle-ID notation each; bind complete resulting certificate bytes in the necroproofed data.
  • Put the necroproof outside data and compute it over the exact current V1 typed-CBOR bytes; do not place an inline V0-style necroproof inside the payload.
  • Bind full organization and bundle UUIDs and the complete certificate list in the necroproofed data.
  • Construct all derivation paths server-side.
  • Do not trust returned path, certificate fingerprint, signer fingerprint, arbitrary index, or arbitrary expiration fields; do not return those fields in the V1 response unless the shared contract is revised.
  • Pin and verify the Caution team signer certificate identity before serving, without returning that identity as a response field.
  • Use a frozen certificate profile and signature verification policy.
  • Apply request-body, count, concurrency, and timeout limits before starting expensive derivation work.
  • Perform blocking cryptographic work outside Axum's async executor threads.
  • Fail closed if current V1 hashing/encoding, necroproof computation, per-certificate signing or post-derivation validation fails.
  • Keep historical Caution team public certificates available to verify bundles after signer/root rotation.
  • Treat schema, certificate/CA-certification profile, signer trust anchor and proof hash format as separate compatibility concerns. A future registry identifier is not a current V1 wire field.

Tests

Unit tests

  • Fixed UUID-to-indices test vectors proving canonical byte order, big-endian conversion, masking, and hardening.
  • Exact complete-path test vectors for multiple organization IDs, bundle IDs, and indices.
  • Organization and bundle components cannot be accidentally swapped.
  • Index zero, maximum supported index, and invalid high-bit indices.
  • Count validation for zero, one, the maximum, and maximum plus one.
  • The derived certificate fingerprint is stable for a fixed test root and path.
  • Different organizations, bundles, or indices yield different fingerprints.
  • Every returned certificate parses, contains no secret material, and has the canonical self-signed index UID with a valid Caution CA certification containing exactly one critical, hashed organization-ID and bundle-ID notation each.
  • The returned certificates vector 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.
  • Fixed fixtures pin exact typed certificate CBOR and canonical-map Keymaker CBOR bytes/hashes. JSON object ordering does not alter typed decoding; Keymaker label insertion order remains hash-independent.
  • Every CA UID certification verifies using the expected Caution team public certificate and authenticates the canonical UID index plus both full UUID notations. The UID self-signature remains required; three context self-notations are not required.
  • Modifying proofed fields or removing/changing UID, notations, self-signature or CA certification fails verification. Independently reject noncritical organization, noncritical bundle and both noncritical notations even when CA signatures remain valid.
  • Verification with an untrusted signer certificate fails.
  • The necroproof is computed over data, is not embedded inline in the payload, and changes whenever any proofed bundle data changes.

Lifecycle and integration tests

  • Service health reports locked before quorum recovery.
  • Fewer than the required shards do not initialize Keyfork.
  • A complete test quorum initializes Keyfork and transitions the service to ready.
  • The root-derived signer certificate matches the test bundle public certificate.
  • A mismatched bundle/root causes a fatal startup failure.
  • A valid request derives exactly certificate_count public certificates for indices 0..count.
  • A generated response, including necroproof and embedded per-certificate CA UID certification signatures, can be verified independently from the HTTP server implementation.
  • Keyfork unavailability during startup or derivation terminates the service or fails the request closed, according to the final lifecycle implementation.
  • Restarting the enclave returns it to locked state and requires recovery again; restarting only HTTP preserves Keyfork root state and invalidates pending approvals.
  • Concurrent requests remain isolated and respect configured limits.

Container tests

  • The deployment Containerfile builds with the Cargo lockfile and an offline compilation step.
  • The final image contains the expected binary, public configuration, and encrypted team bundle, but no plaintext root or test private keys.
  • The service accepts recovery traffic on the configured listener.
  • The HTTP service remains unavailable until recovery completes and works afterward.
  • Termination signals shut down the HTTP task and any Locksmith-owned child resources cleanly.

Remaining implementation and acceptance sequence

  1. Contract implementation, fixed fixtures, critical-notation verification and compatible Platform pins are published; complete current review rather than treating them as unpublished work.
  2. Complete service access/resource hardening and reconcile retained lifecycle/supervision acceptance against Platform-managed bootstrap.
  3. Build the selected release candidate, verify its non-debug PCRs independently, then record issuance, mixed recovery, negative cases and separate HTTP/enclave restart results with real holders.
  4. Track production root/token recovery, migration, capability isolation and effective revocation explicitly; preserve the existing root/CA throughout deployment changes.
  5. Map evidence to this issue's retained criteria and complete review before closure. No current implementation or local test result is a blanket production sign-off.

Acceptance criteria

The service is ready for integration when:

  • It cannot serve derivation requests before Caution team quorum recovery.
  • It derives the signer from the reconstructed root using Keyfork and proves that signer matches the team bundle public certificate.
  • It derives exactly the requested number of certificates at canonical paths computed from signed IDs, the certificate profile, and consecutive indices.
  • No private certificate material crosses the HTTP boundary.
  • It produces a versioned Proofed<PublicCertificateBundle>, valid necroproof over the exact V1 data bytes, and certificates with canonical self-signed index UIDs plus valid Caution CA certifications carrying critical, hashed organization/bundle notations.
  • Independent verification rejects changed IDs/certificate bytes, missing/invalid/noncanonical index UIDs, missing/duplicate/unhashed/noncritical/mismatched organization/bundle notations, invalid UID self-signatures, invalid CA certification, invalid proofs and unsupported artifact shapes. Paths/fingerprints are recomputed rather than trusted from response fields; a general registry identifier is not required by current V1.
  • The dedicated container builds reproducibly and contains no plaintext root material.
  • Restarting only HTTP preserves the existing Keyfork root, invalidates pending approvals and rechecks readiness. Restarting the enclave destroys root availability and requires fresh quorum recovery.

First-test scope and later production work

  • First test: the stricter verifier patch is published/integrated. Retain exact V1 encoding/profile behavior; complete service access/lifecycle acceptance and record exact-revision live results. No security boundary is bypassed for the test.
  • Later: named production custodians/operator assignments, organizational recovery drills, long-term root/signer/credential rotation and rollback procedures, and the general structure-hash registry.
  • New certificate lifetime semantics, additional profile versions or byte-reproducibility promises beyond the frozen fixtures require explicit compatibility decisions. Repeated issuance intentionally generates fresh bundle UUIDs.
  • Bearer issuance authentication and the 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.

## 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`](https://codeberg.org/caution/platform/commit/1f7e2963932ac616ba6c4f0e9553112cb548ce72), [Locksmith `9f702bb`](https://codeberg.org/caution/locksmith/commit/9f702bbf465bc4968e0fa57adcbbc366a1c6977c), [Bootproof `821b5c6`](https://codeberg.org/caution/bootproof/commit/821b5c63e80f082f6d67ba3695c11416933489ec). Platform's dependency/model/mock/default-runtime pins are aligned at Locksmith `d2876e9`; 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](https://codeberg.org/caution/locksmith/src/commit/9f702bbf465bc4968e0fa57adcbbc366a1c6977c/docs/v1-contract.md) and [service hardening](https://codeberg.org/caution/locksmith/src/commit/9f702bbf465bc4968e0fa57adcbbc366a1c6977c/docs/service-hardening.md). Remaining: - Complete actual issuance/network restrictions with [#349](https://codeberg.org/caution/platform/issues/349), while retaining public holder-authorized recryption; review bearer exposure and prove coordinated token rotation. - Harden public recovery/release resource admission with [Locksmith #12](https://codeberg.org/caution/locksmith/issues/12); current worker/body limits do not establish resilience to sustained hostile traffic. - Resolve the retained lifecycle/status/supervision criteria against Platform-managed bootstrap; demonstrate HTTP restart, full enclave restart, same-root recovery and wrong-root failure on the intended deployment. - Explicitly assess shared-root/private-key capability isolation, production custodians, compromise response and root/credential migration. Existing account deletion is not recryptor revocation. These production concerns remain open even though formal operations were deferred beyond the first test. - Triage reachable dependency advisories, finish independent security/fuzz testing, verify the final image/PCRs, and record current acceptance before review/closure. 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` / Locksmith `c33e35e`; 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](https://codeberg.org/caution/locksmith/src/commit/9f702bbf465bc4968e0fa57adcbbc366a1c6977c/docs/key-service-deployment.md). ## Current implemented contract The [public-certificate models](https://codeberg.org/caution/locksmith/src/commit/2da3be50bebd2dfdc4d0d3a94d05f55be02e910c/crates/public-certificate-models/src/lib.rs) define: - Request: tagged `PublicCertificateRequest::V1` with `organization_id: [u8; 16]` and `certificate_count: NonZeroU8`. The caller cannot supply `bundle_id`, paths or indices; the route generates a fresh v4 UUID. - Response: `PublicCertificateResponse = Proofed<PublicCertificateBundle>`, serialized as `{data, necroproof}`. V1 `data` contains canonical 16-byte `organization_id`/`bundle_id` and `certificates: Vec<String>`. - Certificates are public ASCII armor. Count is derived from the vector; no trusted response-level derivation path, fingerprint, signer fingerprint or index object is returned. - The implemented UID is `Caution public certificate index=N`; a standard embedded Caution CA UID certification carries critical `organization-id@caution.co` and `bundle-id@caution.co` notations. Recryption verifies this certification and reads its signed UID index independently of holder ordering. - There is no request `expires_at` or 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. - Consumers verify the outer proof, independently configured CA/PCR policy and certified full UUID/index context. Returned values never establish their own trust anchor. 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 - Build the service in a dedicated `Containerfile.public-cert-service` without changing the lifecycle of the existing Keymaker image. - Require Caution team quorum recovery after every enclave boot or restart; an HTTP-process restart preserves the enclave-owned root and invalidates pending release approvals. - Keep the reconstructed root secret and all derived private key material inside the service enclave. - Derive certificates only within the prescribed `m/pgp'/kmkr'/...` namespace. - Implement the merged `public-certificate-models` request/response contract. - Return public certificates in a versioned `Proofed<PublicCertificateBundle>` response with a top-level necroproof over `data`. - Add the self-signed canonical UID `Caution public certificate index=N` and a standard Caution CA certification over that UID with exactly one critical, hashed organization-ID notation and one critical, hashed bundle-ID notation. - Make the result independently verifiable by the Caution API, Keymaker, and Shard Recryption Service. - Preserve enough version and key identity information in trusted configuration / certificate metadata to support future root, signer, certificate-profile, and derivation-scheme rotations. ## Non-goals - The service does not generate or persist a new random root. - The service does not store the reconstructed Caution team secret in the image or on disk. - The service does not use a separate standalone CA private key. - The service does not run in the same runtime as the self-nuking Keymaker service. - This work does not implement the Keymaker V1 `/generate_quorum` route, Keymaker V1 necroproof changes, or the Shard Recryption Service transport, although it defines the proofed artifact those systems will consume. - Callers cannot submit arbitrary derivation paths, certificate indices, certificate fingerprints, signer fingerprints, or service-internal routing details. ## 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: 1. Locksmith loads the Caution team quorum bundle. 2. Caution team members submit enough shards to meet its threshold. 3. Locksmith reconstructs the root secret in memory. 4. Locksmith initializes `keyforkd`. 5. The service asks Keyfork for the standard Caution team OpenPGP account, currently expected to be `m/pgp'/0'` unless the team chooses a different configured signer path. 6. The service derives its signer certificate and checks that it matches the configured Caution team public certificate / bundle public key. 7. The service becomes ready only after that check succeeds. 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_TOKEN` replaces 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. `/health` returns 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-certificates` accepts only the current V1 request. Platform authorizes the organization; the service generates the bundle UUID and consecutive certificate indices `0..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: ```text m/pgp'/kmkr' ``` `m/pgp'` is the existing Keyfork OpenPGP namespace. The `kmkr` component is constructed with: ```rust u32::from_be_bytes(*b"kmkr") ``` and converted to a hardened `DerivationIndex`. ### UUID conversion For both the organization ID and bundle ID: 1. Use the UUID's canonical 16-byte representation. 2. Split the bytes into four consecutive four-byte chunks. 3. Decode each chunk as a big-endian `u32`. 4. Clear its highest bit with `word & 0x7fff_ffff`. 5. Convert the result to a hardened `DerivationIndex`. 6. Append the four indices in byte order. For certificate index `i`, the complete account path is: ```text m/pgp'/kmkr'/<org-0>'/<org-1>'/<org-2>'/<org-3>'/<bundle-0>'/<bundle-1>'/<bundle-2>'/<bundle-3>'/<i>' ``` The highest-bit removal is intentionally lossy. Consequently: - The necroproofed bundle data must bind the full, unmodified organization and bundle UUIDs. - The organization ID must be authorized by the Caution API and the bundle UUID generated by the certificate service, rather than either being freely supplied by an untrusted caller. - The Shard Recryption Service must verify the full IDs and bundle context, not merely reconstruct and compare the masked path. ### OpenPGP certificate profile The certificate profile is part of the protocol and must be versioned. Subkey ordering matters because `keyfork-derive-openpgp` derives 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: 1. Primary certification key. 2. Signing subkey. 3. Transport- and storage-encryption subkey. 4. Authentication subkey. For each certificate: 1. Request the account XPrv for its complete path from `keyforkd`. 2. Derive the OpenPGP certificate with the current V1 key order and canonical self-signed UID `Caution public certificate index=N`. 3. Add the Caution CA's standard UID certification, containing exactly one hashed, critical `organization-id@caution.co` and one hashed, critical `bundle-id@caution.co` notation with full UUIDs emitted as lowercase hex. 4. Export only public certificate packets; confirm the result parses and retains its self-signed UID and CA-certified context. 5. Return the ASCII-armored certificate in `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>`: versioned `data` plus a separate `necroproof`. Current V1 uses: - Certificate data: SHA-256 of `serde_cbor::to_vec(&PublicCertificateBundle)`; Nitro `user_data` must match, and nonce must be absent/null. - Keymaker data: SHA-256 of CBOR serialization of `serde_cbor::value::to_value(&GenerateQuorumBundle)`; Nitro `user_data` must match. Its nonce is SHA-256 of CBOR serialization of `("keymaker-generate-quorum-necroproof-nonce-v1", bundle_hash.as_slice())`. - Both require independent PCR trust and authenticated generation-time proof verification. Historical provenance never replaces current WebAuthn authorization or destination freshness. 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-models` and `crates/public-cert-service` exist. 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 `locksmithd` working, 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](https://codeberg.org/caution/locksmith/src/commit/2da3be50bebd2dfdc4d0d3a94d05f55be02e910c/crates/public-cert-service/src/derivation.rs). ## Container packaging and production requirements The deployable combined certificate/recryptor example uses [examples/certificate-service/Containerfile](https://codeberg.org/caution/locksmith/src/commit/2da3be50bebd2dfdc4d0d3a94d05f55be02e910c/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](https://codeberg.org/caution/locksmith/src/commit/2da3be50bebd2dfdc4d0d3a94d05f55be02e910c/docs/service-hardening.md). ## Security requirements - Use the zeroizing allocator already used by Keymaker, and explicitly zeroize reconstructed secret buffers where practical. - Do not persist the reconstructed root or any derived private key. - Do not include private OpenPGP packets in responses, traces, errors, or tests. - Use the canonical self-signed UID index and CA certification with exactly one hashed, critical organization-ID and bundle-ID notation each; bind complete resulting certificate bytes in the necroproofed data. - Put the necroproof outside `data` and compute it over the exact current V1 typed-CBOR bytes; do not place an inline V0-style necroproof inside the payload. - Bind full organization and bundle UUIDs and the complete certificate list in the necroproofed data. - Construct all derivation paths server-side. - Do not trust returned path, certificate fingerprint, signer fingerprint, arbitrary index, or arbitrary expiration fields; do not return those fields in the V1 response unless the shared contract is revised. - Pin and verify the Caution team signer certificate identity before serving, without returning that identity as a response field. - Use a frozen certificate profile and signature verification policy. - Apply request-body, count, concurrency, and timeout limits before starting expensive derivation work. - Perform blocking cryptographic work outside Axum's async executor threads. - Fail closed if current V1 hashing/encoding, necroproof computation, per-certificate signing or post-derivation validation fails. - Keep historical Caution team public certificates available to verify bundles after signer/root rotation. - Treat schema, certificate/CA-certification profile, signer trust anchor and proof hash format as separate compatibility concerns. A future registry identifier is not a current V1 wire field. ## Tests ### Unit tests - Fixed UUID-to-indices test vectors proving canonical byte order, big-endian conversion, masking, and hardening. - Exact complete-path test vectors for multiple organization IDs, bundle IDs, and indices. - Organization and bundle components cannot be accidentally swapped. - Index zero, maximum supported index, and invalid high-bit indices. - Count validation for zero, one, the maximum, and maximum plus one. - The derived certificate fingerprint is stable for a fixed test root and path. - Different organizations, bundles, or indices yield different fingerprints. - Every returned certificate parses, contains no secret material, and has the canonical self-signed index UID with a valid Caution CA certification containing exactly one critical, hashed organization-ID and bundle-ID notation each. - The returned `certificates` vector 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. - Fixed fixtures pin exact typed certificate CBOR and canonical-map Keymaker CBOR bytes/hashes. JSON object ordering does not alter typed decoding; Keymaker label insertion order remains hash-independent. - Every CA UID certification verifies using the expected Caution team public certificate and authenticates the canonical UID index plus both full UUID notations. The UID self-signature remains required; three context self-notations are not required. - Modifying proofed fields or removing/changing UID, notations, self-signature or CA certification fails verification. Independently reject noncritical organization, noncritical bundle and both noncritical notations even when CA signatures remain valid. - Verification with an untrusted signer certificate fails. - The necroproof is computed over `data`, is not embedded inline in the payload, and changes whenever any proofed bundle data changes. ### Lifecycle and integration tests - Service health reports locked before quorum recovery. - Fewer than the required shards do not initialize Keyfork. - A complete test quorum initializes Keyfork and transitions the service to ready. - The root-derived signer certificate matches the test bundle public certificate. - A mismatched bundle/root causes a fatal startup failure. - A valid request derives exactly `certificate_count` public certificates for indices `0..count`. - A generated response, including necroproof and embedded per-certificate CA UID certification signatures, can be verified independently from the HTTP server implementation. - Keyfork unavailability during startup or derivation terminates the service or fails the request closed, according to the final lifecycle implementation. - Restarting the enclave returns it to locked state and requires recovery again; restarting only HTTP preserves Keyfork root state and invalidates pending approvals. - Concurrent requests remain isolated and respect configured limits. ### Container tests - The deployment Containerfile builds with the Cargo lockfile and an offline compilation step. - The final image contains the expected binary, public configuration, and encrypted team bundle, but no plaintext root or test private keys. - The service accepts recovery traffic on the configured listener. - The HTTP service remains unavailable until recovery completes and works afterward. - Termination signals shut down the HTTP task and any Locksmith-owned child resources cleanly. ## Remaining implementation and acceptance sequence 1. Contract implementation, fixed fixtures, critical-notation verification and compatible Platform pins are published; complete current review rather than treating them as unpublished work. 2. Complete service access/resource hardening and reconcile retained lifecycle/supervision acceptance against Platform-managed bootstrap. 3. Build the selected release candidate, verify its non-debug PCRs independently, then record issuance, mixed recovery, negative cases and separate HTTP/enclave restart results with real holders. 4. Track production root/token recovery, migration, capability isolation and effective revocation explicitly; preserve the existing root/CA throughout deployment changes. 5. Map evidence to this issue's retained criteria and complete review before closure. No current implementation or local test result is a blanket production sign-off. ## Acceptance criteria The service is ready for integration when: - It cannot serve derivation requests before Caution team quorum recovery. - It derives the signer from the reconstructed root using Keyfork and proves that signer matches the team bundle public certificate. - It derives exactly the requested number of certificates at canonical paths computed from signed IDs, the certificate profile, and consecutive indices. - No private certificate material crosses the HTTP boundary. - It produces a versioned `Proofed<PublicCertificateBundle>`, valid necroproof over the exact V1 `data` bytes, and certificates with canonical self-signed index UIDs plus valid Caution CA certifications carrying critical, hashed organization/bundle notations. - Independent verification rejects changed IDs/certificate bytes, missing/invalid/noncanonical index UIDs, missing/duplicate/unhashed/noncritical/mismatched organization/bundle notations, invalid UID self-signatures, invalid CA certification, invalid proofs and unsupported artifact shapes. Paths/fingerprints are recomputed rather than trusted from response fields; a general registry identifier is not required by current V1. - The dedicated container builds reproducibly and contains no plaintext root material. - Restarting only HTTP preserves the existing Keyfork root, invalidates pending approvals and rechecks readiness. Restarting the enclave destroys root availability and requires fresh quorum recovery. ## First-test scope and later production work - First test: the stricter verifier patch is published/integrated. Retain exact V1 encoding/profile behavior; complete service access/lifecycle acceptance and record exact-revision live results. No security boundary is bypassed for the test. - Later: named production custodians/operator assignments, organizational recovery drills, long-term root/signer/credential rotation and rollback procedures, and the general structure-hash registry. - New certificate lifetime semantics, additional profile versions or byte-reproducibility promises beyond the frozen fixtures require explicit compatibility decisions. Repeated issuance intentionally generates fresh bundle UUIDs. - Bearer issuance authentication and the `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.
anton commented 2026-07-14 22:11:43 +00:00 (Migrated from codeberg.org)

Hmmm what do we do about expiration for these certs that are derived by the public certificates service? @ryansquared

Hmmm what do we do about expiration for these certs that are derived by the public certificates service? @ryansquared
ryansquared commented 2026-07-14 22:31:11 +00:00 (Migrated from codeberg.org)

The service should be one supervised Rust process rather than a shell script that starts a daemon and then starts an HTTP server. The current locksmithd flow should be refactored into reusable library functions so that the new process can supervise both Keyfork and HTTP tasks.

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.

> The service should be one supervised Rust process rather than a shell script that starts a daemon and then starts an HTTP server. The current locksmithd flow should be refactored into reusable library functions so that the new process can supervise both Keyfork and HTTP tasks. 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.
ryansquared commented 2026-07-14 22:33:25 +00:00 (Migrated from codeberg.org)

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.

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.
ryansquared commented 2026-07-14 22:34:06 +00:00 (Migrated from codeberg.org)

Signer fingerprint is automatically in the hashed area of the signature, drop it from the response as well.

Signer fingerprint is automatically in the hashed area of the signature, drop it from the response as well.
ryansquared commented 2026-07-14 22:35:57 +00:00 (Migrated from codeberg.org)

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.

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.
ryansquared commented 2026-07-14 22:38:22 +00:00 (Migrated from codeberg.org)

The manifest does not require a separate CA signature on every derived certificate: the one detached signature authenticates the full UUIDs, derivation paths, indices, fingerprints, and certificate bytes as a single atomic statement.

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.

> The manifest does not require a separate CA signature on every derived certificate: the one detached signature authenticates the full UUIDs, derivation paths, indices, fingerprints, and certificate bytes as a single atomic statement. 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.
ryansquared commented 2026-07-14 22:39:52 +00:00 (Migrated from codeberg.org)

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.

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.
caution-bot commented 2026-07-14 22:40:23 +00:00 (Migrated from codeberg.org)

Updated the issue body to incorporate the review comments:

  • Added explicit certificate expiration to the request, signed payload, certificate profile, validation rules, tests, and acceptance criteria.
  • Clarified that Locksmith remains responsible for shard recovery and Keyfork startup; the public certificate service waits for Locksmith/Keyfork readiness rather than separately supervising keyforkd.
  • Removed derivation_path, certificate fingerprint, and signer_fingerprint from the response contract; verifiers recompute paths/fingerprints and rely on OpenPGP signature metadata plus configured trust anchors.
  • Replaced the one-off RFC 8785/JCS requirement with a prerequisite shared Caution signed-structure format and identifier registry, so this service follows the platform-wide deterministic hashing/signature standard.
Updated the issue body to incorporate the review comments: - Added explicit certificate expiration to the request, signed payload, certificate profile, validation rules, tests, and acceptance criteria. - Clarified that Locksmith remains responsible for shard recovery and Keyfork startup; the public certificate service waits for Locksmith/Keyfork readiness rather than separately supervising `keyforkd`. - Removed `derivation_path`, certificate `fingerprint`, and `signer_fingerprint` from the response contract; verifiers recompute paths/fingerprints and rely on OpenPGP signature metadata plus configured trust anchors. - Replaced the one-off RFC 8785/JCS requirement with a prerequisite shared Caution signed-structure format and identifier registry, so this service follows the platform-wide deterministic hashing/signature standard.
ryansquared commented 2026-07-14 22:41:15 +00:00 (Migrated from codeberg.org)

In short, we should basically be replacing "signature" with "necroproof".

In short, we should basically be replacing "signature" with "necroproof".
caution-bot commented 2026-07-14 22:43:22 +00:00 (Migrated from codeberg.org)

Caught the two review comments that arrived just before my previous update and revised the design again:

  • Added per-certificate Caution team CA signatures to the response contract, tests, security requirements, and acceptance criteria so the Shard Recryption Service can verify each derived certificate itself.
  • Changed the response shape to a data envelope plus a separate necroproof over data, explicitly avoiding the old v0 inline-necroproof layout.
  • Updated independent verification expectations to check the necroproof, manifest signature, and every per-certificate CA signature, while still deriving paths/fingerprints locally rather than trusting returned fields.
Caught the two review comments that arrived just before my previous update and revised the design again: - Added per-certificate Caution team CA signatures to the response contract, tests, security requirements, and acceptance criteria so the Shard Recryption Service can verify each derived certificate itself. - Changed the response shape to a `data` envelope plus a separate `necroproof` over `data`, explicitly avoiding the old v0 inline-necroproof layout. - Updated independent verification expectations to check the necroproof, manifest signature, and every per-certificate CA signature, while still deriving paths/fingerprints locally rather than trusting returned fields.
caution-bot commented 2026-07-14 22:50:04 +00:00 (Migrated from codeberg.org)

Audited the full comment thread and made one final correction for the later necroproof guidance:

  • The bundle-level signature field is now removed entirely; the bundle-level authenticity check is the top-level necroproof over data.
  • Per-certificate Caution team CA signatures remain on each derived certificate for Shard Recryption Service verification.
  • The design now consistently describes shared deterministic structure hashing for necroproofs/signatures instead of a one-off manifest signature scheme.

I rechecked the issue body for all requested markers: expires_at, Locksmith-owned Keyfork startup, omitted derivation_path/fingerprint/signer_fingerprint fields, shared structure-hash registry, per-certificate ca_signature, top-level data/necroproof, and no remaining bundle-level "signature" response field.

Audited the full comment thread and made one final correction for the later necroproof guidance: - The bundle-level `signature` field is now removed entirely; the bundle-level authenticity check is the top-level `necroproof` over `data`. - Per-certificate Caution team CA signatures remain on each derived certificate for Shard Recryption Service verification. - The design now consistently describes shared deterministic structure hashing for necroproofs/signatures instead of a one-off manifest signature scheme. I rechecked the issue body for all requested markers: `expires_at`, Locksmith-owned Keyfork startup, omitted `derivation_path`/`fingerprint`/`signer_fingerprint` fields, shared structure-hash registry, per-certificate `ca_signature`, top-level `data`/`necroproof`, and no remaining bundle-level `"signature"` response field.
ryansquared commented 2026-07-14 22:52:48 +00:00 (Migrated from codeberg.org)

The CA signature should be a UID signature in the standard OpenPGP format, placed upon the derived certificate itself.

The CA signature should be a UID signature in the standard OpenPGP format, placed upon the derived certificate itself.
caution-bot commented 2026-07-14 22:56:32 +00:00 (Migrated from codeberg.org)

Updated the design for the latest OpenPGP-format clarification:

  • Removed the separate ca_signature response field from certificate entries.
  • Clarified that the Caution team CA signature is a standard OpenPGP UID certification signature embedded on the derived certificate itself.
  • Updated verification/tests/security language so consumers parse each returned certificate and verify its embedded CA UID certification, while the top-level necroproof still covers the data envelope.
Updated the design for the latest OpenPGP-format clarification: - Removed the separate `ca_signature` response field from certificate entries. - Clarified that the Caution team CA signature is a standard OpenPGP UID certification signature embedded on the derived certificate itself. - Updated verification/tests/security language so consumers parse each returned certificate and verify its embedded CA UID certification, while the top-level necroproof still covers the `data` envelope.
caution-bot commented 2026-07-18 22:48:13 +00:00 (Migrated from codeberg.org)

Updated after the public certificate contract branch merged into caution/locksmith main (12d5f17).

Key corrections:

  • public-certificate-models now exists, so #383 no longer asks to create that model crate.
  • The current V1 contract uses [u8; 16] canonical UUID bytes for organization_id and bundle_id.
  • The current V1 request has organization_id, bundle_id, and certificate_count; it does not include expires_at.
  • The current V1 response is Proofed<PublicCertificateBundle> with top-level data + necroproof.
  • The V1 bundle has organization_id, bundle_id, and certificates: Vec<String>; certificate indices are embedded as certificate self-notations, not response-level fields.
  • The service implementation should now target the merged model crate and treat expiration as a contract-follow-up decision rather than an already-present field.
Updated after the public certificate contract branch merged into `caution/locksmith` `main` (`12d5f17`). Key corrections: - `public-certificate-models` now exists, so #383 no longer asks to create that model crate. - The current V1 contract uses `[u8; 16]` canonical UUID bytes for `organization_id` and `bundle_id`. - The current V1 request has `organization_id`, `bundle_id`, and `certificate_count`; it does **not** include `expires_at`. - The current V1 response is `Proofed<PublicCertificateBundle>` with top-level `data` + `necroproof`. - The V1 bundle has `organization_id`, `bundle_id`, and `certificates: Vec<String>`; certificate indices are embedded as certificate self-notations, not response-level fields. - The service implementation should now target the merged model crate and treat expiration as a contract-follow-up decision rather than an already-present field.
caution-bot commented 2026-07-18 23:09:19 +00:00 (Migrated from codeberg.org)

Updated the certificate binding language from the latest Control Tower notes:

  • Each generated cert now explicitly has a dedicated Caution Key-style UID.
  • That UID carries critical notations for organization ID, bundle ID, and key index.
  • The certificate self-signature and the Caution team CA UID certification both cover the same UID/notation data.
  • The response still returns only armored cert strings; org/bundle/index bindings are verified from the OpenPGP certificate internals plus the top-level necroproof, not trusted response metadata.

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.

Updated the certificate binding language from the latest Control Tower notes: - Each generated cert now explicitly has a dedicated `Caution Key`-style UID. - That UID carries critical notations for organization ID, bundle ID, and key index. - The certificate self-signature and the Caution team CA UID certification both cover the same UID/notation data. - The response still returns only armored cert strings; org/bundle/index bindings are verified from the OpenPGP certificate internals plus the top-level necroproof, not trusted response metadata. 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.
caution-bot commented 2026-07-19 01:11:20 +00:00 (Migrated from codeberg.org)

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-service crate/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.

Opened the first Locksmith-side implementation PR for this issue: - https://codeberg.org/caution/locksmith/pulls/13 This is intentionally a foundation PR rather than a full close: it adds the dedicated `public-cert-service` crate/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.
vkobel commented 2026-09-17 15:03:55 +00:00 (Migrated from codeberg.org)

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:

  • Lifecycle: health unconditionally reports ready; restarting only the HTTP service does not reset the independently running Keyfork root.
  • Issuance controls: the derivation route lacks API-only authentication and concurrency/duration limits; the example permits public ingress.
  • Hash contract: implementation uses local SHA-256 over serialized CBOR, without the specified shared structure identifier/compatibility registry.

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.

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: - Lifecycle: health unconditionally reports ready; restarting only the HTTP service does not reset the independently running Keyfork root. - Issuance controls: the derivation route lacks API-only authentication and concurrency/duration limits; the example permits public ingress. - Hash contract: implementation uses local SHA-256 over serialized CBOR, without the specified shared structure identifier/compatibility registry. 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.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
public/platform#383
No description provided.