Skip to content

mirror_worker: load cosigner + ticket keys, /metadata, extended state DO#238

Draft
lukevalenta wants to merge 1 commit into
lvalenta/mirror-worker-skeletonfrom
lvalenta/mirror-worker-keys-state
Draft

mirror_worker: load cosigner + ticket keys, /metadata, extended state DO#238
lukevalenta wants to merge 1 commit into
lvalenta/mirror-worker-skeletonfrom
lvalenta/mirror-worker-keys-state

Conversation

@lukevalenta
Copy link
Copy Markdown
Contributor

Summary

Builds on the mirror_worker skeleton (#237) toward c2sp.org/tlog-mirror#add-entries. This PR ships the cryptographic plumbing and DO state extensions add-entries will 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 main once #237 lands.

What's in this PR

Mirror cosigner key (C1)

  • New MirrorSigner enum dispatching on the OID embedded in the MIRROR_SIGNING_KEY PKCS#8 PEM secret:
    • id-Ed25519MirrorSigner::CosignatureV1 (Ed25519, cosignature/v1).
    • id-ml-dsa-44MirrorSigner::SubtreeV1 (ML-DSA-44, subtree/v1).
  • Both variants carry the signer plus the DER-encoded SPKI for the matching verifying key, computed once at construction and reused by /metadata.
  • Pattern lifted from the witness's sign-subtree work in Add ML-DSA-44 / sign-subtree support to witness_worker #229; the algorithm surface is intentionally identical because both consume tlog-cosignatures at the spec layer.
  • OnceLock<MirrorSigner> cache so the PKCS#8 parse + ML-DSA-44 expansion happens at most once per worker isolate.

Ticket key (C1)

  • load_ticket_macer reads the MIRROR_TICKET_KEY secret as standard base64 (RFC 4648 §4), decodes to exactly 32 bytes, constructs a tlog_mirror::TicketMacer for HMAC-SHA-256-128 authentication of opaque tickets.
  • Allowed-dead until consumed by the add-entries handler (C4).

GET /metadata (C1)

JSON MetadataResponse:

Field Type Notes
mirror_name string from config.<env>.json
description string | omitted omitted when None
mirror_public_key base64 string DER-encoded SPKI
mirror_algorithm string "cosignature/v1" or "subtree/v1"
submission_prefix string
monitoring_prefix string falls back to submission_prefix
logs[] array sorted by origin (deterministic)

The sort matters for diff-based monitoring of /metadata and for any client that hashes the body for cache keys. The metadata_logs helper is extracted so the sort order is unit-testable without a worker::Env.

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 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.

Extended MirrorState DO (C2)

  • New CommittedCheckpoint { size, hash, signed_note_bytes } per origin under storage key "committed", alongside the existing PendingCheckpoint. Stores the full signed-note bytes so <monitoring>/<encoded-origin>/checkpoint (a future PR) can serve them with the mirror's cosignature appended.
  • New RPC POST /get-state — read-only MirrorStateSnapshot { pending, committed }. Used by the future add-entries handler for early 409/404 reject before reading the streaming request body.
  • New RPC POST /commit — atomic, monotone advance of committed:
Condition Result
body.size > pending.size 400 (cannot commit beyond pending)
body.size < committed.size 200 with current committed (no write — spec forbids rolling back; concurrent add-entries already advanced past us)
otherwise advance, return new committed state

Atomicity comes from the DO input/output gates, same pattern as /update-pending.

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" (dev key is ML-DSA-44), log list contains the configured origin. wait_for_mirror also retargets to /metadata for readiness probing.

Stats

  • 7 files changed, +875 / −37.
  • 18 unit tests in mirror_worker (was 5; +13 from this PR: 3 signer dispatch, 2 dev-vars pin, 4 metadata wire shape, 4 extended DO state).
  • 11 unchanged in mirror_worker_config.
  • 1 #[tokio::test] integration test (excluded from default run).

Verification

All five pre-push checks pass locally on the head commit:

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

Integration test requires wrangler dev running; not executed here.

What this PR does NOT do

  • POST /add-entries handler (C4).
  • Subtree consistency proof verification helper (C3).
  • add-entries error paths (C5).
  • Read interface (GET /<encoded-origin>/checkpoint, /tile/..., /<entries>/...) — separate PR.
  • Cleaner DO for partial-tile cleanup — separate PR.

Branch / merge order

Stacked PR; base will auto-retarget to main once #237 (mirror_worker skeleton + add-checkpoint) merges.

@lukevalenta lukevalenta self-assigned this May 5, 2026
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
@lukevalenta lukevalenta force-pushed the lvalenta/mirror-worker-skeleton branch from ea45297 to 74cc416 Compare May 5, 2026 19:30
@lukevalenta lukevalenta force-pushed the lvalenta/mirror-worker-keys-state branch from 1d139f5 to 94f81c3 Compare May 5, 2026 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant