mirror_worker: load cosigner + ticket keys, /metadata, extended state DO#238
Draft
lukevalenta wants to merge 1 commit into
Draft
mirror_worker: load cosigner + ticket keys, /metadata, extended state DO#238lukevalenta wants to merge 1 commit into
lukevalenta wants to merge 1 commit into
Conversation
Builds on the `mirror_worker` skeleton (#237) toward [c2sp.org/tlog-mirror#add-entries][add-e]. This PR ships the cryptographic plumbing and DO state extensions `add-entries` will need; the handler itself remains a future PR. Slice references below are anchored in #186 (comment), which lists the C1–C5 sub-slice plan for `add-entries`. Mirror cosigner key (C1) - New `MirrorSigner` enum dispatching on the OID embedded in the `MIRROR_SIGNING_KEY` PKCS#8 PEM secret: * `id-Ed25519` → `MirrorSigner::CosignatureV1` (`cosignature/v1`, Ed25519). * `id-ml-dsa-44` → `MirrorSigner::SubtreeV1` (`subtree/v1`, ML-DSA-44). Both variants carry the signer plus the DER-encoded `SubjectPublicKeyInfo` for the matching verifying key, computed once at construction and reused by `/metadata`. - `load_mirror_signer`/`build_mirror_signer`/`load_mirror_public_key_der` helpers, mirroring the witness pattern. The signer is held as a `OnceLock<MirrorSigner>` so the PKCS#8 parse + ML-DSA-44 expansion happens at most once per worker isolate. - Pattern lifted from the witness's sign-subtree work (`crates/witness_worker/src/lib.rs` on `lvalenta/witness-worker-sign-subtree`); the mirror's algorithm surface is intentionally identical because both consume tlog-cosignatures at the spec layer. Ticket key (C1) - New `load_ticket_macer` reading the `MIRROR_TICKET_KEY` secret as standard base64 (RFC 4648 §4), decoding to exactly 32 bytes, constructing a `tlog_mirror::TicketMacer` for HMAC-SHA-256-128 authentication of opaque tickets. Cached in a `OnceLock<TicketMacer>` like the cosigner. Allowed-dead until consumed by the `add-entries` handler (C4). Dev secrets (C1) - `crates/mirror_worker/.dev.vars` ships an ML-DSA-44 dev cosigner key (PKCS#8 PEM, seed `[0x42; 32]`, same pattern as the dev witness's sign-subtree key) and a 32-byte dev ticket key (`[0x37; 32]` base64-encoded). Two new `dev_config_tests` pin that both load cleanly via `build_mirror_signer` / `base64::decode` so an operator typo in `.dev.vars` is caught at unit-test time, not at the first request to `wrangler dev`. GET /metadata (C1) - New endpoint serving a JSON `MetadataResponse` with `mirror_name`, optional `description`, DER-encoded `mirror_public_key`, `mirror_algorithm` (`"cosignature/v1"` or `"subtree/v1"`), `submission_prefix`, `monitoring_prefix`, and a sorted-by-origin list of trusted-log entries. Three unit tests pin the wire shape: `description` omitted-when-None, `description` present-when-Some, `mirror_algorithm` field present. - `metadata_logs` helper extracted from the handler so the sort order is unit-testable without a `worker::Env`. Sort by origin makes the response deterministic across worker isolates, regardless of `HashMap` iteration order — matters for diff-based monitoring of `/metadata` and for any client that hashes the body for cache keys. One unit test pins the sort. Extended MirrorState DO (C2) - New `CommittedCheckpoint` (`size`, `hash`, `signed_note_bytes`) under storage key `"committed"`, alongside the existing `PendingCheckpoint`. Stores the full signed-note bytes for the committed (mirror) checkpoint so `<monitoring>/<encoded-origin>/checkpoint` (a future PR) can serve them with the mirror's cosignature appended. - New `MirrorStateSnapshot { pending, committed }` returned by `POST /get-state`. Read-only; used by the future `add-entries` handler for early 409/404 reject before reading the streaming request body. - New `POST /commit` RPC carrying `CommitRequest` (`size`, `hash`, `signed_note_bytes`). Atomic, monotone advance of `committed`: * `size > pending.size` → 400 (cannot commit beyond pending). * `size < committed.size` → 200 with current state, no write (concurrent `add-entries` already advanced past us; spec forbids rolling back). * Otherwise → advance and return the new committed state. Atomicity comes from the DO input/output gates, same pattern as the existing `/update-pending` RPC. - Four new unit tests: `CommittedCheckpoint` JSON-format pin, default-zero, snapshot wire shape, `CommitRequest` JSON-format pin. Integration test update - Step 1 retargeted from `GET /` to `GET /metadata` since `/metadata` is now wired. Asserts `mirror_name`, non-empty SPKI, `mirror_algorithm == "subtree/v1"` (the dev key is ML-DSA-44), log list contains the configured origin. `wait_for_mirror` also retargets to `/metadata` for readiness probing. Pre-push checks all green: `cargo clippy --workspace --all-targets -- -Dwarnings -Dclippy::pedantic`, `cargo test`, `cargo fmt --all --check`, `cargo machete`, `cargo check -p mirror_worker --target wasm32-unknown-unknown`. 18 unit tests in `mirror_worker` (was 5), 11 unchanged in `mirror_worker_config`, 1 integration test (excluded from default run). [add-e]: https://c2sp.org/tlog-mirror#add-entries
ea45297 to
74cc416
Compare
1d139f5 to
94f81c3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds on the
mirror_workerskeleton (#237) toward c2sp.org/tlog-mirror#add-entries. This PR ships the cryptographic plumbing and DO state extensionsadd-entrieswill need; the handler itself is a future PR.Slice references (C1, C2 below) are anchored in the tlog-mirror plan comment on #186. Stacked on #237; base will retarget to
mainonce #237 lands.What's in this PR
Mirror cosigner key (C1)
MirrorSignerenum dispatching on the OID embedded in theMIRROR_SIGNING_KEYPKCS#8 PEM secret:id-Ed25519→MirrorSigner::CosignatureV1(Ed25519,cosignature/v1).id-ml-dsa-44→MirrorSigner::SubtreeV1(ML-DSA-44,subtree/v1)./metadata.OnceLock<MirrorSigner>cache so the PKCS#8 parse + ML-DSA-44 expansion happens at most once per worker isolate.Ticket key (C1)
load_ticket_macerreads theMIRROR_TICKET_KEYsecret as standard base64 (RFC 4648 §4), decodes to exactly 32 bytes, constructs atlog_mirror::TicketMacerfor HMAC-SHA-256-128 authentication of opaque tickets.add-entrieshandler (C4).GET /metadata(C1)JSON
MetadataResponse:mirror_nameconfig.<env>.jsondescriptionNonemirror_public_keymirror_algorithm"cosignature/v1"or"subtree/v1"submission_prefixmonitoring_prefixsubmission_prefixlogs[]The sort matters for diff-based monitoring of
/metadataand for any client that hashes the body for cache keys. Themetadata_logshelper is extracted so the sort order is unit-testable without aworker::Env.Dev secrets (C1)
crates/mirror_worker/.dev.varsships an ML-DSA-44 dev cosigner key (PKCS#8 PEM, seed[0x42; 32], same pattern as the dev witness's sign-subtree key) and a 32-byte dev ticket key ([0x37; 32]base64-encoded).dev_config_testspin both load cleanly viabuild_mirror_signer/base64::decode, so an operator typo in.dev.varsis caught at unit-test time, not at the first request towrangler dev.Extended
MirrorStateDO (C2)CommittedCheckpoint { size, hash, signed_note_bytes }per origin under storage key"committed", alongside the existingPendingCheckpoint. Stores the full signed-note bytes so<monitoring>/<encoded-origin>/checkpoint(a future PR) can serve them with the mirror's cosignature appended.POST /get-state— read-onlyMirrorStateSnapshot { pending, committed }. Used by the futureadd-entrieshandler for early 409/404 reject before reading the streaming request body.POST /commit— atomic, monotone advance ofcommitted:body.size > pending.sizebody.size < committed.sizeadd-entriesalready advanced past us)Atomicity comes from the DO input/output gates, same pattern as
/update-pending.Integration test update
Step 1 retargeted from
GET /toGET /metadatasince/metadatais now wired. Assertsmirror_name, non-empty SPKI,mirror_algorithm == "subtree/v1"(dev key is ML-DSA-44), log list contains the configured origin.wait_for_mirroralso retargets to/metadatafor readiness probing.Stats
mirror_worker(was 5; +13 from this PR: 3 signer dispatch, 2 dev-vars pin, 4 metadata wire shape, 4 extended DO state).mirror_worker_config.#[tokio::test]integration test (excluded from default run).Verification
All five pre-push checks pass locally on the head commit:
Integration test requires
wrangler devrunning; not executed here.What this PR does NOT do
POST /add-entrieshandler (C4).add-entrieserror paths (C5).GET /<encoded-origin>/checkpoint,/tile/...,/<entries>/...) — separate PR.Branch / merge order
Stacked PR; base will auto-retarget to
mainonce #237 (mirror_workerskeleton +add-checkpoint) merges.