Date: 2026-06-02. Scope: every specification cactus depends on,
as catalogued in specs/README.md — the Merkle Tree Certificate draft
and its tiled-log profile, the C2SP transparency-log specs, the X.509 /
PKIX stack, the NIST algorithm standards, and the ACME / TLS / HTTP
layer.
This document maps each specification to the cactus code that
implements it and records the conformance state: what cactus honors,
the deliberate choices it makes, and the parts of the draft it does not
implement. Section numbers reference
draft-ietf-plants-merkle-tree-certs-04 unless otherwise noted. Cactus is
ML-DSA-44-only (no ECDSA cosigning), builds on Go 1.27's crypto/mldsa,
and targets the draft plus the MTC-with-tlog profile
(specs/mtc-tlog-draft.md).
| Spec | Relevance | What cactus honors |
|---|---|---|
| draft-…-merkle-tree-certs-04 | Defines everything | §4 subtree math, §5 log entries/cosigners, §6 cert + landmark assembly, §7.2 verification, §9 ACME extensions, Appendix A ASN.1 |
| mtc-tlog-draft.md (profile) | High | CA-prefix-URL serving, /<log number>/ tlog-tiles layout, checkpoint origin = log ID as oid/1.3.6.1.4.1.…, cosigner-name derivation, /landmarks URL, ML-DSA-44-only cosigning |
| RFC 9162 (CT v2) | High | §2.1.1 leaf/interior hash prefixes (0x00/0x01) and the proof algorithms §4 specialises |
| Spec | Relevance | What cactus honors |
|---|---|---|
| signed-note | High | Signature-line format base64(keyID ‖ signature); key-ID SHA-256(name ‖ 0x0A ‖ sigType ‖ pubkey)[:4] |
| tlog-checkpoint | High | /checkpoint body: origin / size / base64 root |
| tlog-cosignature | High | ML-DSA-44 cosigned_message (12-byte subtree/v1\n\0 label); note-line timestamped_signature wrapper |
| tlog-tiles | High | Tile path layout, .p/<W> partials, uint16-length-prefixed entry bundles, checkpoint endpoint |
| tlog-witness (PR #245) | High | POST /sign-subtree request/response framing + the CA-signature DoS gate |
| tlog-mirror | Medium | Follower consistency semantics (cactus uses a polling tile-follower, not the push API) |
| tlog-proof | Low | Offline inclusion-proof format; cactus ships the equivalent inside the X.509 MTCProof instead |
| Spec | Relevance | What cactus honors |
|---|---|---|
| RFC 5280 (PKIX) | High | §4.1.2.2 serial positivity/length, §4.1.2.5 UTCTime<2050 cutoff, §4.1.2.7 SPKI, §4.2 extension criticality |
| RFC 9881 (ML-DSA in PKIX) | High | id-ml-dsa-44 OID, absent parameters, raw FIPS 204 key in the SPKI BIT STRING, pure-mode signatures |
| RFC 9925 (Unsigned X.509) | High | id-alg-unsigned + zero-length signatureValue on the CA certificate |
| X.690 (DER) | High | Definite/minimal lengths, BIT STRING unused-bits octet, SET-OF ordering |
| RFC 5912 (PKIX ASN.1) | Low | Reference for AlgorithmIdentifier / Extensions / Validity types; no code maps to it directly |
| RFC 6960 (OCSP) | Trivial | Cited only for the domain-separation argument (cosignature label ≠ ResponseData SEQUENCE); cactus does not implement OCSP |
| Spec | Relevance | What cactus honors |
|---|---|---|
| FIPS 204 (ML-DSA) | High | Pure-mode ML-DSA-44 with empty context; 32-byte key-gen seed |
| FIPS 180-4 (SHA-2) | High | SHA-256 for tree hashing, SPKI/key-ID hashing, HKDF PRF |
| RFC 8032 (Ed25519) | Low | Only as an accepted ACME account-key JWS algorithm via go-jose |
| FIPS 186-5 (DSS) | Background | ECDSA is not used for cactus's own signing; relevant only to ECDSA ACME account keys |
| Spec | Relevance | What cactus honors |
|---|---|---|
| RFC 8555 (ACME) | High | §6 JWS/nonce/url/Content-Type, §7 resource state machine, §7.4 finalize/badCSR, plus the MTC §9 download extensions |
| draft-…-tls-trust-anchor-ids-03 | Medium | §3 TrustAnchorID binary rep, §4.1 opaque<1..2^8-1> cap, §6 property-list encoding, §6.1 PEM layout |
| RFC 9110 (HTTP) | Medium | Retry-After, 503, Accept negotiation |
| RFC 4648 (base64) | High | base64url (no pad) for ACME JWS; standard base64 for C2SP note bodies |
| RFC 7807 (Problem Details) | Medium | ACME error documents (application/problem+json) |
| RFC 7638 (JWK Thumbprint) | Medium | Account identity + key authorization |
| RFC 3339 (timestamps) | Low | ACME expires / notBefore / notAfter |
| RFC 1035 (DNS names) | Low | dns identifier validation |
| RFC 8446 (TLS 1.3) | Low | §3 presentation language MTC wire formats inherit |
| RFC 3629 (UTF-8) | Trivial | UTF8String DN attribute value |
| RFC 2119 / RFC 8174 | Trivial | Requirement-keyword boilerplate |
- Subtree math (MTC §4) —
tlogx/subtree.go,inclusion.go, andconsistency.goimplement §4.1 validity, §4.3.2 inclusion-proof evaluation, §4.4 consistency proofs (generation and verification), and §4.5FindSubtrees, over the RFC 9162 §2.1.10x00/0x01hash prefixes. - Entry hashing and §7.2 verification —
cert/entry.gobuilds the TBSCertificateLogEntry and the single-pass leaf hash;cert/rpverify.goperforms §7.2 steps 1–12, including the landmark trusted-subtree fast path and the revoked-range check.
cosigned_message(cert/proof.go) matches tlog-cosignature: the 12-bytesubtree/v1\n\0label, cosigner_name,timestamp = 0, log_origin, start, end, and hash.- Key-ID derivation (
cert/keyid.go) isSHA-256(name ‖ 0x0A ‖ 0x06 ‖ pubkey)[:4]for ML-DSA-44, with the signed-note0xffescape for the experimental -65/-87 cosigners. The public-key length is validated against the FIPS 204 size for the algorithm. - Note-line signature values are
keyID ‖ timestamped_signature, wheretimestamped_signatureisu64 timestamp ‖ signaturewith a zero timestamp for MTC subtree cosignatures (tlog-cosignature). This applies to the checkpoint signature line (log/note.go), the sign-subtree response (mirror/server.go), and the CA DoS-gate line in a sign-subtree request (cert/cosigner_request.go); the matching parsers strip the timestamp and reject a non-zero timestamp on a subtree cosignature. The MTCProof signature carried in the X.509 certificate is a bare PKIX signature over theCosignedMessage(§6.2), with no note-line wrapper. - Checkpoint body (
log/note.go) is origin / size / base64 root, with the empty tree's root being the RFC 6962 / RFC 9162 §2.1 empty-tree hashSHA-256("")(log/tilewriter, not the zero hash). Tile and entry-bundle framing (log/tilewriter,tile/server.go) uses the tlog-tiles path layout,.p/<W>partials, and uint16 length-prefixed entry bundles. POST /sign-subtree(mirror/server.go) implements the tlog-witness request/response framing —subtree <start> <end>, base64 hash, 0–8 cosignature lines, 0–63 consistency-proof lines, blank line, reference checkpoint (≤8 signatures) — parsing the decimal range and size with no leading zeros. The DoS gate requires a valid CA subtree cosignature, returning 403 when it is missing or fails to verify. In CA mode the sign-subtree requests carry the CA's own subtree cosignature (cmd/cactus/main.go) so mirrors that enforce the gate by default honour them. The mirror follower (mirror/follower.go) polls an upstream over tlog-tiles and verifies append-only progress.
- Checkpoint origins and cosigner names are
oid/1.3.6.1.4.1.<relative-OID>(cert/proof.go,cert/trustanchorid.go); the issuance-log ID iscaID.0.logNumber(cert/caid.go); each log is served under the CA prefix at/<log number>/…with/landmarksat/<log number>/landmarks(tile/server.go,cmd/cactus/main.go); and cosigning is ML-DSA-44 only.
- RFC 5280 — serial number is
(logNumber<<48)|index, always positive, non-zero, and minimally encoded (cert/caid.go,ca/issuer.go); validity uses UTCTime below the year 2050 and GeneralizedTime at or above it; basicConstraints, keyUsage, and id-pe-mtcCertificationAuthority are marked critical. - RFC 9881 — the
id-ml-dsa-44OID is emitted with absent parameters and the raw FIPS 204 key in the SPKI BIT STRING (cert/rpverify.go); the SPKI parser requires the AlgorithmIdentifier OID to equal the configured sig-alg and rejects present parameters. - RFC 9925 — the CA certificate uses
id-alg-unsignedwith a zero-lengthsignatureValue(cert/cacert.go). - X.690 — hand-rolled DER uses definite, minimal lengths and a correct BIT STRING unused-bits octet.
- Leaf extensions — only subjectAltName, keyUsage, and
extendedKeyUsage are copied from the CSR onto the leaf; other
extensions (basic constraints, the MTC-CA extension, private
extensions) are dropped, duplicate extension OIDs are rejected
(RFC 5280 §4.2), and an empty subject forces a critical subjectAltName
(§4.1.2.6) — all in
ca/validator.go.
- ML-DSA-44 signing and verification use pure mode with an empty context
(FIPS 204 / RFC 9881); keys derive deterministically from a 32-byte
seed via HKDF-SHA256 with a per-algorithm info string
(
signer/mldsa.go). SHA-256 is used consistently for tree hashing and for SPKI/key-ID hashing. Verification is SHA-256-only: the relying-party config rejects a CA certificate advertising any otherlogHash(cert/rpverify.go).
- The TrustAnchorID binary representation is the RELATIVE-OID content
octets (
cert/trustanchorid.go; e.g.32473.1 → 81 fd 59 01), used on the wire fields that require it, while the relative dotted-decimal ASCII form backs theoid/…names and the DN attribute value. - The CertificatePropertyList is sorted by type, rejects duplicates, and
frames each value as
opaque<0..2^16-1>(cert/properties.go). Theapplication/pem-certificate-chain-with-propertiesbody places theCERTIFICATE PROPERTIESblock first and the certificate second, per trust-anchor-ids §6.1.
- §6 JWS (
acme/handler.go,acme/jose.go) — the protectedurlis checked against the request URL; nonces are single-use withbadNonceon failure; thealgallow-list (RS256/ES256/384/512/EdDSA, nononeor MAC algorithms) drivesbadSignatureAlgorithmwith thealgorithmsfield; a non-application/jose+jsonbody is rejected with 415; and jwk/kid are mutually exclusive. - §6.3 POST-as-GET — certificate download requires a signed JWS with
an empty payload, checks account ownership (returning 401
unauthorizedon mismatch), and issues a freshReplay-Nonce. - §6.5.1 — the
Replay-Nonceis the base64url encoding of random octets (acme/state.go). - §7.1.2 account resource —
POST /account/{id}serves the account object (POST-as-GET) with the requiredordersmember, andPOST /account/{id}/ordersserves the orders list. - §7.4 finalize — the request body and CSR are parsed before the
atomic ready→processing claim, so a malformed finalize never strands
the order in
processing(§7.1.6); the race loser getsorderNotReady, CSR errors map tobadCSR, and a malformednotBefore/notAfter(RFC 3339) is rejected withmalformed. The order moves tovalidonce its entry is sequenced (MTC §9). - Object fields — a
validchallenge carriesvalidated(§8), avalidauthorization carriesexpires(§7.1.4), and aninvalidorder or challenge carries anerrorproblem document (§7.1.6/§8). - §9 download extensions — the
CertificatePropertyListcarries thetrust_anchor_id, the alternate URL returns 503 + Retry-After until a covering landmark exists and the landmark-relative certificate thereafter, and the error documents follow RFC 7807. Account-key thumbprints and key authorizations use the RFC 7638 SHA-256 thumbprint.
- Experimental placeholder OIDs —
id-alg-mtcProof,id-rdna-trustAnchorID, andid-pe-mtcCertificationAuthorityuse the1.3.6.1.4.1.44363.47.*private arc pending IANA assignment, matching the draft's experimental status (cert/oid.go). - ASCII TrustAnchorID in the DN UTF8String — the "for initial
experimentation" form (
cert/dn.go,cert/oid.go); the binary RELATIVE-OID representation is used on the wire fields that require it. - Pull-mode mirror —
mirror/follower.gopolls an upstream over tlog-tiles and verifies append-only progress by root recomputation; it does not implement the tlog-mirror push API (add-checkpoint/add-entries). - Challenge modes —
auto-passis a test-only mode;http-01is real; DNS-01 is not implemented. - FIPS 186-5 — ECDSA is not used for cactus's own signing, so 186-5 is relevant only to ECDSA ACME account keys handled by go-jose.
- Log pruning (MTC §5.2.3 / the profile's pruning rules) is not implemented.
- Revocation by serial range (§7.5) is enforced during verification
(§7.2 step 4, seeded from the CA cert's
minSerial); ingesting additional revoked ranges out-of-band is not implemented. - Account update / deactivation (RFC 8555 §7.3.2/§7.3.6) is not implemented; the account resource is read-only beyond creation.
- DN SET-OF ordering (
cert/dn.go) is correct by single-element construction; canonical SET ordering would be needed only if a second RDN attribute were added.
Fully implemented: §4.1–§4.5 subtrees (validity, inclusion + consistency proof generation/verification, FindSubtrees) · §5.1 CA / log parameters · §5.2 log IDs + entries (null + tbs_cert_entry, ASN.1 module, single-pass hash) · §5.3/§5.3.1 cosigner signature format · §5.5 CA cosigner + CA-certificate extension · §6.2 certificate format / MTCProof / serial · §6.3 standalone certs · §6.4.1–§6.4.3 landmark sizes, allocation, publishing, and landmark-relative cert construction · §7.1/§7.2/§7.4/§7.5 relying-party verification (config from CA cert, verify steps 1–12, trusted subtrees, revoked-range data model) · §9 ACME extensions · Appendix A ASN.1 module (experimental-OID form). The MTC-with-tlog profile's serving layout, identity mapping, and ML-DSA-44-only requirement are honored.
Partial / not implemented: §5.2.3 log pruning (none) · §7.3 trusted cosigner policy (single CA cosigner + N mirrors; no quorum-of-roles engine) · §7.5 revocation (stub list) · ACME account update / deactivation (not implemented).
Out of scope: §8 use in TLS — the TLS stack lives elsewhere.
Specs: all of specs/ (the groups in specs/README.md).
Code (non-test):
acme/{handler,jose,keyauth,persist,readdir,state,types}.goca/{issuer,validator}.gocert/{cacert,caid,cosigner_request,dn,entry,keyid,landmark,oid,proof,properties,rpverify,sigverify,trustanchorid,verify}.gocmd/cactus/main.go,cmd/cactus-cli/{main,treeverify}.go,cmd/cactus-keygen/main.goconfig/config.golandmark/{handler,sequence}.golog/{log,note}.go,log/tilewriter/tilewriter.gomirror/{follower,note,note_parse,server}.gosigner/{mldsa,seed,signer}.gotile/server.gotlogx/{consistency,inclusion,subtree}.go
The 22 integration/*_test.go end-to-end tests (TestParallelIssuance,
TestRelyingPartyFastPath, TestEndToEndCAWithThreeMirrors,
TestMirrorRestartResume, and the binary smoke tests) exercise the
issuance, verification, and mirror-cosignature paths and serve as the
de-facto fixture suite, since the draft itself ships no test vectors.