Skip to content

Implement IETF MTC CA#214

Draft
lukevalenta wants to merge 7 commits intomainfrom
lvalenta/ietf-mtc-v2
Draft

Implement IETF MTC CA#214
lukevalenta wants to merge 7 commits intomainfrom
lvalenta/ietf-mtc-v2

Conversation

@lukevalenta
Copy link
Copy Markdown
Contributor

@lukevalenta lukevalenta commented Apr 15, 2026

Summary

Implements the IETF MTC CA on top of the generic tlog_subtree_signature crate from #221.

  • Adds ietf_mtc_api and ietf_mtc_worker crates (initially copied from the
    bootstrap_mtc counterparts, then stripped of bootstrap-specific functionality
    and adapted for draft-ietf-plants-merkle-tree-certs-02).
  • Adds standalone certificate support (§6.2), ML-DSA-44 signing alongside
    Ed25519, and helper scripts for workers.dev deployment.
  • The final commit wires ietf_mtc_api onto Add tlog_subtree_signature crate with sign-subtree wire-format and binary-format support #221's tlog_subtree_signature
    crate: MtcSigningKey / MtcVerifyingKey impl the crate's RawSigner /
    RawVerifier traits, MtcSubtreeNoteVerifier delegates to
    SubtreeNoteVerifier, and the duplicate mtc-subtree/v1 binary-format
    helper in ietf_mtc_api::cosigner is removed.

Stacked on

Testing

cargo clippy --workspace --all-targets -- -Dwarnings -Dclippy::pedantic,
cargo test, cargo fmt --all --check, cargo machete all pass locally.
Integration test job integration-ietf-mtc exercises both ML-DSA-44 and
Ed25519 logs against wrangler dev.

@lukevalenta lukevalenta self-assigned this Apr 15, 2026
@lukevalenta lukevalenta force-pushed the lvalenta/ietf-mtc-v2 branch 10 times, most recently from cb29508 to 9e0d790 Compare April 17, 2026 12:43
@lukevalenta lukevalenta added the mtc Merkle Tree Certificates label Apr 17, 2026
@lukevalenta lukevalenta force-pushed the lvalenta/ietf-mtc-v2 branch 3 times, most recently from 8d2e9fb to 1600761 Compare April 17, 2026 13:54
@lukevalenta lukevalenta changed the title Lvalenta/ietf mtc v2 Implement IETF MTC CA Apr 17, 2026
@lukevalenta lukevalenta force-pushed the lvalenta/ietf-mtc-v2 branch 4 times, most recently from be8a89a to 9fd74be Compare April 21, 2026 15:26
…nary-format support

Introduces a new spec-level crate, separate from `ietf_mtc_api`, for the
`sign-subtree` cosigner endpoint. The endpoint is currently specified in
draft-ietf-plants-merkle-tree-certs-02 §C.2; the intent is for it to
migrate into a standalone C2SP specification (provisional URL
`c2sp.org/tlog-subtree-signature`), and this crate is positioned to ship
with that future spec.

Scope:

  HTTP wire format (top-level module):

  Server side (cosigner receiving the request):
  - `SignSubtreeRequest`, `SubtreeNoteBody`
  - `parse_sign_subtree_request`
  - `serialize_sign_subtree_response`

  Client side (requester):
  - `serialize_subtree_note_body`
  - `serialize_sign_subtree_request`
  - `parse_sign_subtree_response`

  Shared:
  - `MAX_CONSISTENCY_PROOF_HASHES = 63`
  - `TlogSubtreeSignatureError`

  Binary signing format (`crypto` module, re-exported at crate root):

  - `serialize_subtree_signature_input` — produce the `mtc-subtree/v1\n\0`
    binary message a subtree signer actually signs (§5.4.1). Opaque-bytes
    interface: `cosigner_id` and `log_id` are `&[u8]` so the crate doesn't
    depend on any particular identity encoding (MTC uses BER-encoded
    `TrustAnchorID`; future C2SP spec may use something else).
  - `RawSigner` / `RawVerifier` traits: algorithm-agnostic abstraction
    over "produce raw sig bytes for a message" / "check raw sig bytes for
    a message". Concrete implementations (Ed25519, ML-DSA-44, ...) live
    in downstream crates; today the only workspace ones are in
    `ietf_mtc_api` (`MtcSigningKey` / `MtcVerifyingKey`).
  - `sign_subtree(signer, cosigner_id, log_id, start, end, hash)` —
    convenience wrapper that builds the binary input and hands it to the
    signer.
  - `SubtreeNoteVerifier<V: RawVerifier>` — a `signed_note::NoteVerifier`
    that accepts a subtree signed note, reconstructs the binary signing
    input from the note text body, and delegates to the wrapped
    `RawVerifier`. Caller supplies `KeyName`, key ID, and identity bytes;
    this crate does not prescribe name-format or key-ID conventions.

The label `mtc-subtree/v1` is kept as-is for wire compatibility with
current IETF MTC deployments. When the C2SP spec settles on a new label
(e.g. via [C2SP#237]'s `subtree/v1`), `serialize_subtree_signature_input`
will gain a second encoding and existing clients can migrate in lockstep.

Out of scope (stays in `ietf_mtc_api`):

  - Multi-algorithm key enums (`MtcSigningKey` / `MtcVerifyingKey`). They
    become `RawSigner` / `RawVerifier` impls in a follow-up commit.
  - `TrustAnchorID` / `ID_RDNA_TRUSTANCHOR_ID` and the MTC `oid/…`
    key-name scheme. The new crate takes an opaque `KeyName` + key ID.
  - `MtcCosigner` and `ParsedMtcProof`. These remain MTC-CA-specific.

Tests: 21 unit tests (17 wire-format + 4 binary-format) pin the request /
response round-trip, malformed-input rejection, the `mtc-subtree/v1`
binary layout, and the `SubtreeNoteVerifier` happy-path + malformed-text
paths. The binary-format layout test mirrors the pattern used by
`SequenceMetadata::serialize_cache_entries` — any change to the format
would fail loudly and visibly.

The `parse_sign_subtree_request` doc comment carries forward a TODO from
the original `ietf_mtc_api::sign_subtree` module: the `\n\n` blank-line
separator between sections is ambiguous with the intra-note
text/signatures boundary. Two cleaner alternatives (length-prefixed
sections; TLS-binary framing) are sketched for upstream discussion.

Follow-ups:

  - `lvalenta/ietf-mtc-v2` drops the duplicate parsers +
    `serialize_mtc_subtree_signature_input` from `ietf_mtc_api`; its
    `MtcCosigner::sign_subtree` becomes a thin wrapper over
    `tlog_subtree_signature::sign_subtree`, and `MtcSubtreeNoteVerifier`
    becomes a thin wrapper that fixes the MTC identity convention in
    place around `SubtreeNoteVerifier`.
  - `lvalenta/tlog-witness` gains a `/sign-subtree` endpoint using this
    crate's parsers + `ietf_mtc_api` for the MTC signing semantics.

[C2SP#237]: C2SP/C2SP#237
Initial scaffolding for the IETF MTC implementation
(draft-ietf-plants-merkle-tree-certs). Copied directly from
bootstrap_mtc_api and bootstrap_mtc_worker as a starting point, incorporating
the SequencerMetadata refactor; subsequent commits will remove bootstrap-
specific functionality and implement draft-02 behaviour.

Identifiers (crate names, paths, module names, types prefixed BootstrapMtc,
constants prefixed BOOTSTRAP_MTC, kebab-case references) are renamed for the
new crate; file contents otherwise match the bootstrap source byte-for-byte.
The integration_tests crate gains tests/ietf_mtc_api.rs, which still uses the
bootstrap_mtc_api client/fixtures until the strip commit introduces IETF-
specific counterparts.
Removes bootstrap-specific code from ietf_mtc_api and ietf_mtc_worker and
replaces it with IETF draft-ietf-plants-merkle-tree-certs-02 functionality:

ietf_mtc_api:
- AddEntryRequest: replace chain (Vec<Vec<u8>>) with csr (base64url DER)
- build_pending_entry: parse PKCS#10 CSR, extract SAN extension
- TbsCertificateLogEntry: no outer SEQUENCE wrapper (davidben-10); adds
  subject_public_key_info_algorithm field (plants-02)
- encode_fields/decode_fields: manual DER encoding/decoding without
  #[derive(Sequence)]
- serialize_landmark_relative_cert: renamed from serialize_signatureless_cert
- Remove GetRootsResponse, validate_chain, validate_correspondence, etc.

ietf_mtc_worker:
- Remove ccadb_roots_cron, ct_logs_cron, dev-bootstrap-roots.pem
- Remove ROOTS OnceLock, load_roots, sct_validator dep, get-roots route
- add_entry: call build_pending_entry with CSR instead of validate_chain
- IetfMtcSequenceMetadata::leaf_index() / timestamp() accessors used in
  frontend response construction (in place of tuple field access)
- wrangler.jsonc: cleaned up for IETF worker
- config/src/lib.rs: replace enable_sct_validation with version: DraftVersion
- config.schema.json: replace enable_sct_validation with version field
- config/Cargo.toml: add ietf_mtc_api dep (needed for DraftVersion type)

integration_tests:
- Add IetfMtcClient with CSR-based add_entry
- Add make_ietf_mtc_csr fixture using x509-cert RequestBuilder
- Update tests/ietf_mtc_api.rs to use IetfMtcClient and CSR submission
- Add ietf_mtc_api to Cargo.toml

READMEs updated for all four MTC crates.
Implements draft-ietf-plants-merkle-tree-certs §6.2 standalone MTC
certificates, where the signatureValue embeds a cosignature over the
subtree covering the leaf.

ietf_mtc_api:
- cosigner.rs: promote Ed25519-only cosigner to a multi-algorithm type:
  - MtcSigningKey / MtcVerifyingKey enums with Ed25519 variant and an
    MlDsa44 stub (unimplemented! until draft-03 / C2SP#237 align on the
    subtree/v1 unified signature format)
  - MtcCheckpointNoteVerifier / MtcSubtreeNoteVerifier: split the single
    NoteVerifier into two types with distinct key IDs (mtc-checkpoint/v1
    vs mtc-subtree/v1) and distinct note-body parsers, so each role is
    self-describing instead of overloading a single verifier
  - ParsedMtcProof: parse an MTCProof from a certificate's signatureValue
    and verify its cosignature against a subtree hash / verifying key
- lib.rs: add SUBTREE_SIG_KEY_PREFIX / subtree_sig_key / SignedSubtree;
  rename serialize_landmark_relative_cert -> serialize_mtc_cert and add a
  cosignatures parameter (empty = landmark-relative, non-empty = standalone)
- AddEntryResponse: replace individual fields with a single certificate
  field (DER-encoded standalone cert)
- relative_oid.rs: add from_ber_bytes and Debug/PartialEq/Eq/Hash derives

ietf_mtc_worker:
- IetfMtcSequenceMetadata gains old_tree_size / new_tree_size fields. The
  frontend uses Subtree::split_interval(old, new) plus the leaf_index to
  identify the single R2 key of the cached subtree signature, avoiding
  the candidate-subtree enumeration used in bootstrap
- sequencer_do: checkpoint_callback signs and caches the covering
  subtree(s) with sign_and_cache_batch_subtrees / sign_and_cache_landmark_subtrees
- cleaner_do: remove stale subtree signatures from R2
- frontend_worker: build_standalone_cert fetches the cached SignedSubtree,
  computes the inclusion proof, and assembles a DER-encoded §6.2 standalone
  cert via serialize_mtc_cert
- The /metadata endpoint now returns the cosigner's DER-encoded SPKI so
  multi-algorithm clients can distinguish key types

tlog_tiles:
- evaluate_subtree_inclusion_proof: complement to
  verify_subtree_inclusion_proof that returns the derived subtree hash
  (so callers can compare it against an external commitment like a cosignature)

integration_tests:
- IetfMtcClient::get_signed_subtree fetches cached signatures from R2
- AddEntryResponse: replace field parsing with the certificate-only shape
- tests/ietf_mtc_api.rs: full draft-02 §7.2 standalone certificate
  verification end-to-end
Implements the MlDsa44 variants that were previously stubs in
MtcSigningKey / MtcVerifyingKey (unimplemented!). Keeps the same key ID
scheme (mtc-checkpoint/v1 / mtc-subtree/v1 context strings) and the same
mtc-subtree/v1 binary signing format as draft-02 specifies — no C2SP#237
(subtree/v1) changes.

ietf_mtc_api:
- MtcSigningKey::MlDsa44(ExpandedSigningKey<MlDsa44>) and
  MtcVerifyingKey::MlDsa44(VerifyingKey<MlDsa44>)
- try_sign / verify / to_public_key_der routed through the ml-dsa crate's
  sign/verify APIs and RustCrypto PKCS#8 SPKI encoding

ietf_mtc_worker:
- parse_key_pair now dispatches on the PKCS#8 AlgorithmIdentifier OID
  (1.3.101.112 Ed25519 / 2.16.840.1.101.3.4.3.17 ML-DSA-44) so logs can be
  configured with either algorithm

Dev / CI:
- .dev.vars: replace dev1's Ed25519 key with a NIST FIPS 204 ML-DSA-44
  test vector; drop the unused WITNESS_KEY_* entries left over from the
  bootstrap scaffold. dev2 stays on Ed25519 to exercise both paths.
- config.dev.json: give dev1 the same short landmark_interval_secs as dev2
  so the full test suite runs under either algorithm.
- Integration CI: run the IETF MTC test job twice (IETF_MTC_LOG_NAME=dev1
  then =dev2) to cover both algorithms per push.

Integration tests:
- fetch_verifying_key dispatches on the SPKI algorithm OID and constructs
  either an Ed25519 or ML-DSA-44 MtcVerifyingKey.
- get_certificate_returns_valid_cert no longer skips on non-dev2 log names.
…rkers.dev deployment

scripts/create-log.sh provisions the R2 bucket and uploads the signing
key secret for a single log shard. ALGORITHM selects ed25519 or
ml-dsa-44; the ML-DSA-44 path passes `-provparam
ml-dsa.output_formats=seed-only` so the generated PEM is the compact
32-byte-seed PKCS#8 form that the worker's `ml-dsa-0.1.0-rc.8`-based
PKCS#8 decoder expects (default OpenSSL output would be the expanded-key
form which the worker would reject).

Unlike the ct_worker create-log.sh this does not create a witness key —
IETF MTC has no witness-key concept.

scripts/test-deployment.sh is an end-to-end smoke test against a deployed
worker: it generates a CSR (ed25519 or ML-DSA-44), submits it via
/add-entry, fetches the returned standalone MTC certificate, then polls
for a landmark-relative certificate for the same entry. Prints a summary
(size, subject/issuer/validity, signatureAlgorithm OID) for each cert but
does not verify the cosignatures or inclusion proofs — for that, run the
Rust integration tests against the same BASE_URL.

The README gains a Deployment section covering workers.dev (using the
existing `dev` environment in `wrangler.jsonc`) that references both
scripts, and pointing at the ct_worker docs for custom-domain deployments.
The `sign-subtree` wire-format parsers/serializers and the
`mtc-subtree/v1` binary signing layout live in the
`tlog_subtree_signature` crate (introduced earlier in this branch's
history). Refactor `ietf_mtc_api` to build on top of that crate rather
than duplicate it:

- `ietf_mtc_api::sign_subtree` becomes a thin re-export shim over
  `tlog_subtree_signature`, re-exporting `SignSubtreeRequest`,
  `SubtreeNoteBody`, `MAX_CONSISTENCY_PROOF_HASHES`,
  `parse_sign_subtree_request` / `serialize_sign_subtree_response`
  (server side) and `serialize_subtree_note_body` /
  `serialize_sign_subtree_request` / `parse_sign_subtree_response`
  (client side). All wire-format unit tests now live in the
  `tlog_subtree_signature` crate.

- `MtcSubtreeNoteVerifier` is kept in this crate (it owns the
  MTC-specific `oid/{id_rdna_trustanchor_id}.{log_id}` name format and
  the `mtc-subtree/v1` key-ID context string) but delegates its
  `NoteVerifier` impl to a wrapped
  `tlog_subtree_signature::SubtreeNoteVerifier<MtcVerifyingKey>`.
  Taking `&TrustAnchorID` parameters now so the constructor fits
  pedantic-clippy without needless moves; no external callers yet.

- `MtcSigningKey` / `MtcVerifyingKey` implement the algorithm-agnostic
  `tlog_subtree_signature::RawSigner` / `RawVerifier` traits so they
  can plug directly into
  `tlog_subtree_signature::{sign_subtree, SubtreeNoteVerifier}`.

- `MtcCosigner::sign_subtree` is now a one-liner over
  `tlog_subtree_signature::sign_subtree`, eliminating the duplicated
  `serialize_mtc_subtree_signature_input` helper from `cosigner.rs`.
  `MtcCheckpointNoteVerifier::verify` and
  `ParsedMtcProof::verify_cosignature` use
  `tlog_subtree_signature::serialize_subtree_signature_input` as the
  single source of truth for the `mtc-subtree/v1\n\0` binary layout.

Net effect: `ietf_mtc_api` gains a dep on `tlog_subtree_signature`
and loses several hundred lines of duplicated wire-format code and
tests. No public-API breakage besides `MtcSubtreeNoteVerifier::new`
switching from owned to borrowed `TrustAnchorID` arguments (no
external callers).
@lukevalenta lukevalenta force-pushed the lvalenta/ietf-mtc-v2 branch from 9fd74be to b00c111 Compare April 21, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mtc Merkle Tree Certificates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant