A standalone Go service implementing
Merkle Tree Certificates (MTC)
per draft-ietf-plants-merkle-tree-certs-01. It supports spec-compliant
signatureless certificates (signatureAlgorithm = id-alg-mtcProof), multi-cosigner
subtree signing (Ed25519 + ML-DSA post-quantum), batch/landmark infrastructure,
and a local ACME CA with two certificate modes. It can also extend a DigiCert
Private CA by watching its MariaDB database for certificate issuances and
revocations, constructing an append-only issuance log as a Merkle tree, and
serving it via the C2SP tlog-tiles HTTP protocol.
Internal / experimental. Not for production use.
This project implements a substantial portion of draft-ietf-plants-merkle-tree-certs-01.
The primary certificate mode produces spec-compliant MTC certificates where
signatureAlgorithm = id-alg-mtcProof (OID 1.3.6.1.4.1.44363.47.0) and the
signatureValue field carries the binary MTCProof. A legacy mode using custom
X.509 extensions is also available for backward compatibility.
| Feature | Spec Reference | Notes |
|---|---|---|
| MTC certificate format | MTC §4 | signatureAlgorithm = id-alg-mtcProof, signatureValue = MTCProof |
| TBSCertificateLogEntry | MTC §5.3 | ASN.1 structure with SHA-256(SPKI) instead of full public key |
| MerkleTreeCertEntry encoding | MTC §5.3 | TLS presentation language: 2-byte (uint16) type + 3-byte length + contents octets |
| MTCProof binary format | MTC §5.4 | uint64 start/end, length-prefixed inclusion proof + signatures |
| MTCSignature encoding | MTC §5.4 | TrustAnchorID cosigner_id<1..255> + opaque signature<0..2^16-1> |
| Multi-cosigner subtree signing | MTC §5.4.1 | Spec-compliant mtc-subtree/v1 signing format |
| ML-DSA post-quantum cosigning | MTC §5.5 | ML-DSA-44/65/87 via cloudflare/circl alongside Ed25519 |
| Batch/subtree infrastructure | MTC §5.4 | Batch accumulation with multi-cosigner subtree signing |
| Landmark verification | MTC §5.6 | Signatureless mode: verify against known tree_size → root_hash |
| Signed + signatureless modes | MTC §4 | Auto-detect: cosigner signatures (signed) or landmarks (signatureless) |
| Append-only issuance log | MTC §5.3 | Full X.509 DER certificates + TBSCertificateLogEntry entries |
| Merkle tree construction | RFC 9162 §2 | SHA-256(0x00 ∥ data) for leaves, SHA-256(0x01 ∥ left ∥ right) for interior nodes |
| C2SP tlog-tiles HTTP API | tlog-tiles | /checkpoint, /tile/<L>/<N>, /tile/entries/<N> |
| Signed checkpoints | C2SP signed-note | Ed25519 signatures in signed-note format |
| Inclusion proofs | RFC 9162 §2.1.3 | GET /proof/inclusion?serial=<hex> endpoint |
| Consistency proofs | RFC 9162 §2.1.4 | GET /proof/consistency?old=M&new=N — append-only audit |
| Revocation tracking | MTC §5.7 | Revocation-by-index bitfield, polled from CA database |
| Cosigner signing | MTC §5.5 | Ed25519 + ML-DSA-44/65/87 key pairs for checkpoints and subtrees |
| Null entry at index 0 | MTC §5.3 | Sentinel entry per spec |
| Assertion bundles | — | Self-contained proof artifacts (JSON + PEM) with cert metadata |
| X.509 metadata extraction | — | Parses DER certificates for display in bundles and UI |
| Certificate browser | — | Admin UI for searching/browsing certs with status badges |
| Visualization explorer | — | Sunburst, treemap, and proof explorer with 4 color modes |
mtc-assertion CLI |
— | Standalone tool to fetch, verify, and inspect assertion bundles |
| Proactive assertion generation | — | Background pipeline pre-computes bundles after each checkpoint |
| Proof freshness management | — | Detects and regenerates stale proofs as the tree grows |
| Assertion polling endpoint | — | GET /assertions/pending for downstream consumers |
| Webhook notifications | — | Push notifications with HMAC-SHA256 signing when assertions are ready |
| Assertion statistics | — | GET /assertions/stats + admin dashboard metrics |
| ACME server (RFC 8555) | MTC §7 | Standalone ACME endpoint on separate port with full order lifecycle |
| JWS request verification | RFC 7515 | ES256 + RS256 with JWK and KID authentication |
| Account management | RFC 8555 §7.3 | Create/lookup accounts by JWK thumbprint |
| Order lifecycle | RFC 8555 §7.4 | pending → ready → processing → valid with authorization + challenge flow |
| http-01 challenge validation | RFC 8555 §8.3 | Auto-approve mode for internal CAs, real HTTP validation path |
| CA proxy (finalize) | — | Proxies CSR to DigiCert CA REST API, polls for assertion bundle |
| Certificate + assertion download | — | PEM certificate with appended assertion bundle proof |
| TLS assertion stapling | — | Server staples assertions via SCT field; client verifies inline |
| TLS verification client | — | Extracts + verifies Merkle proof from TLS handshake (MTC + legacy) |
| Embedded inclusion proofs (legacy) | RFC 6962 | MTC proof in custom X.509 extension (backward-compat mode) |
| Two-phase certificate signing | — | Pre-cert → Merkle hash → re-sign with proof (legacy mode) |
| Local intermediate CA | — | ECDSA P-256 self-signed CA with local signing control |
mtc-verify-cert CLI |
— | Offline verification of MTC-spec and legacy certificates (auto-detect) |
| Pre-certificate log entries | — | entry_type=2 for canonical TBSCertificate DER |
| Feature | Spec Reference | Why |
|---|---|---|
| External cosigner protocol | MTC §5.5 | Requires distributed coordination infrastructure |
| TLS 1.3 custom extension | MTC §6 | Go crypto/tls does not support custom extensions; demo uses SCT field |
| Browser relying-party logic | MTC §8 | Requires browser/client-side implementation |
┌─────────────────┐ ┌──────────────────┐
│ DigiCert CA │ read │ mtc-bridge │
│ MariaDB 10.11 │◄────────│ (Go service) │
│ :3306 │ │ │
│ digicert_ca DB │ │ ┌─ watcher ──┐ │
└─────────────────┘ │ │ poll every │ │
│ │ 10s for new │ │
│ │ certs/revs │ │
│ └─────┬───────┘ │
│ │ │
│ ┌─────▼───────┐ │
│ │ issuancelog │ │
│ │ append entry │ │
│ │ update tree │ │──► PostgreSQL State DB
│ │ checkpoint │ │ (mtcbridge, :5432)
│ └─────┬───────┘ │
│ │ │
│ │ │
│ ┌─────▼───────┐ │
│ │ assertion │ │
│ │ issuer │ │ On each checkpoint:
│ │ (Phase 2) │ │ batch-build bundles,
│ └─────┬───────┘ │ refresh stale proofs
│ │ │
│ ┌─────▼───────┐ │
│ │ tlogtiles │ │──► HTTP :8080
│ │ admin UI │ │ /checkpoint, /tile/...
│ │ proofs │ │ /proof/inclusion
│ │ assertions │ │ /assertion/{query}
│ │ polling │ │ /assertions/pending
│ └──────┬──────┘ │
│ │ │
│ ┌────▼────┐ │
│ │webhooks │ │──► POST to configured URLs
│ │(optional│ │ HMAC-SHA256 signed
│ └────┬────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ ACME server │ │──► HTTP :8443
│ │ (Phase 3) │ │ /acme/directory
│ │ RFC 8555 │ │ /acme/new-account
│ │ JWS verify │──│──► DigiCert CA REST API
│ │ CA proxy │ │ (finalize → issue cert)
│ │ │ │
│ │ ┌─ local CA─┤ │ Phase 5 (opt-in):
│ │ │ 2-phase │ │ Pre-cert → hash → re-sign
│ │ │ signing │ │ with MTC proof in X.509 ext
│ │ └───────────┘ │
│ └─────────────┘ │
└──────────────────┘
Data flow: The watcher polls the DigiCert CA's MariaDB for new certificates and revocations. Each new certificate is appended to the Merkle tree in PostgreSQL. Checkpoints are signed with Ed25519 and served over HTTP alongside tile data and inclusion proofs. The ACME server (Phase 3) runs on a separate port and provides RFC 8555 certificate issuance — it proxies finalize requests to the DigiCert CA, then waits for the assertion issuer to build the inclusion proof bundle before delivering the certificate with its MTC proof.
Local CA — MTC-spec mode (primary): When local_ca.enabled = true and
local_ca.mtc_mode = true, the ACME server issues spec-compliant MTC
certificates. The certificate's signatureAlgorithm is id-alg-mtcProof
(OID 1.3.6.1.4.1.44363.47.0) and signatureValue carries the binary
MTCProof (subtree range, inclusion proof, cosigner signatures). Log entries
use TBSCertificateLogEntry with a SHA-256 hash of the SPKI instead of the
full public key, per the spec. Verification can be done in signed mode
(cosigner signatures) or signatureless mode (landmark root hashes).
Local CA — legacy mode: When local_ca.enabled = true without mtc_mode,
the ACME server uses two-phase signing to embed the proof as a custom X.509
extension (OID 1.3.6.1.4.1.99999.1.1). This mode is kept for backward
compatibility. Both modes can be verified offline using mtc-verify-cert,
which auto-detects the certificate format.
- DigiCert ONE Private CA — a provisioned Private CA instance with:
- API Key — REST API authentication key (from DigiCert ONE admin console)
- CA ID — identifier for the issuing CA (hex string, e.g.,
A76AC522CBABC804919211EB5706CFAD) - Template ID — certificate template configured for TLS issuance (hex string)
- REST API endpoint — typically
http://<ca-host>/certificate-authority/api/v1
- MariaDB 10.11+ — the CA's backing database (
digicert_caschema)- mtc-bridge needs read-only
SELECTaccess to thecertificateandcatables - Default credentials:
<DB_USERNAME>/<DB_PASSWORD>on port 3306 - The CA database must be reachable on the
digicert-ca_defaultDocker network
- mtc-bridge needs read-only
- Docker Desktop and Docker Compose (for containerized deployment)
- Go 1.21+ (only if building locally outside Docker)
- PostgreSQL 16 — provided automatically by
docker-compose.ymlon port 5432 curl,openssl,python3for the walkthrough commands
The DigiCert CA database runs on a separate Docker network. Create it before running mtc-bridge if it doesn't already exist:
# Create the external network (if the CA isn't already running via Docker Compose)
docker network create digicert-ca_defaultmtc-bridge connects to two networks: mtc-internal (PostgreSQL) and
digicert-ca_default (MariaDB CA database).
Copy .env.example to .env and fill in your DigiCert credentials:
cp .env.example .env| Variable | Description | Example |
|---|---|---|
CA_API_KEY |
DigiCert REST API key | abc123... |
CA_ID |
Issuing CA identifier | A76AC522CBABC804919211EB5706CFAD |
CA_TEMPLATE_ID |
Certificate template ID | 0196198F96545084143B237D9E39FC90 |
CA_URL |
CA API base URL | http://digicert-ca:8080 |
MTC_CADB_HOST |
MariaDB hostname | ca-db |
MTC_CADB_PASSWORD |
MariaDB password | (required) |
For local certificate issuance (no DigiCert CA dependency):
make generate-local-ca # generates keys/local-ca.key + keys/local-ca.pemThen set local_ca.enabled: true in config.yaml. For MTC-spec certificates
(recommended), also set local_ca.mtc_mode: true. Clients must trust the local
CA root certificate (keys/local-ca.pem).
# 1. Clone the repo
git clone https://github.com/briantrzupek/ca-extension-merkle.git
cd ca-extension-merkle
# 2. Build
make build
# 3. Generate a cosigner key (first time only)
make generate-key
# 4. Configure environment (copy and edit .env with your CA credentials)
cp .env.example .env
# Edit .env with your DigiCert CA API key, CA ID, and template ID
# 5. Generate self-signed TLS certs for the ACME server
./gen-demo-cert.sh
# 6. (Optional) Generate local CA for MTC-spec certificate mode
make generate-local-ca
# Then set local_ca.enabled: true and local_ca.mtc_mode: true in config.yaml
# 7. Start all services via Docker Compose
docker compose up -d
# Or run locally (requires PostgreSQL on localhost:5432):
make run
# Or: ./bin/mtc-bridge -config config.yamlThe service starts on http://localhost:8080. It will immediately begin
ingesting certificates from the CA database and building the Merkle tree.
The ACME server starts on https://localhost:8443 (TLS with self-signed cert).
This is the recommended demo path. It generates a spec-compliant MTC certificate
where signatureAlgorithm = id-alg-mtcProof and the signatureValue carries the
binary MTCProof — no traditional signature at all.
make demo-mtcThis builds the binaries and runs a complete end-to-end MTC certificate demo:
- Generates an ECDSA P-256 key pair and CSR
- Builds a
TBSCertificateLogEntrywith SHA-256(SPKI) per the MTC spec - Wraps it in a
MerkleTreeCertEntry(TLS presentation language encoding) - Builds a Merkle tree and computes the inclusion proof
- Constructs an
MTCProof(signatureless mode — no cosigner signatures) - Builds the final certificate with
signatureAlgorithm = id-alg-mtcProof - Verifies the certificate end-to-end
=== MTC-Spec Certificate Demo ===
Domain: demo.example.com
Step 1: Generating ECDSA P-256 key pair and CSR...
Step 2: Building TBSCertificateLogEntry (SPKI hashed per MTC spec)...
Step 3: Wrapping in MerkleTreeCertEntry (type=1, TLS encoding)...
Step 4: Building Merkle tree and computing inclusion proof...
Step 5: Building MTC certificate (signatureAlgorithm = id-alg-mtcProof)...
=== MTC Certificate Details ===
Serial (leaf index): 0
Subject: demo.example.com
Signature Algorithm: id-alg-mtcProof (1.3.6.1.4.1.44363.47.0)
MTCProof:
Subtree range: [0, 1)
Inclusion proof: 0 sibling hashes (single-leaf tree)
Signatures: 0 (signatureless mode)
Step 6: Verifying MTC certificate...
=== Verifying MTC-Spec Certificate ===
Written to: /tmp/mtc-spec-cert.pem
Verify: ./bin/mtc-verify-cert -cert /tmp/mtc-spec-cert.pem
=== Verifying MTC-Spec Certificate ===
Subject: CN=demo.example.com
Serial: 0
Issuer: CN=MTC Demo CA,O=MTC Demo,C=US
Format: MTC-spec (id-alg-mtcProof)
Subtree: [0, 1)
Leaf Index: 0
MTC Proof Verification:
[PASS] Inclusion proof valid (subtree root matches)
Mode: signatureless (no cosigner signatures)
All checks passed.
The MTC certificate looks like a standard X.509 certificate, but with a fundamentally different trust model:
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 0 ← leaf index in the Merkle tree
Signature Algorithm: 1.3.6.1.4.1.44363.47.0 ← id-alg-mtcProof
Issuer: CN=MTC Demo CA, O=MTC Demo, C=US
Validity: ...
Subject: CN=demo.example.com
Subject Public Key Info: ...
Signature Algorithm: 1.3.6.1.4.1.44363.47.0
Signature Value: ← binary MTCProof, not a signature
00:00:00:00:00:00:00:00:00:00:00: (subtree start + end + inclusion proof)
MTC certificates support two verification modes:
-
Signatureless mode: Verify the inclusion proof against a known landmark (tree_size → root_hash mapping). No cosigner signatures needed. This is the most efficient mode and what
make demo-mtcdemonstrates. -
Signed mode: Verify cosigner signatures on the subtree root hash. Supports multiple cosigners with Ed25519 and/or ML-DSA (post-quantum) algorithms.
# Offline verification (auto-detects MTC-spec vs legacy format)
./bin/mtc-verify-cert -cert cert.pem
# With bridge checkpoint comparison
./bin/mtc-verify-cert -cert cert.pem -bridge-url http://localhost:8080The legacy embedded proof demo generates certificates with a custom X.509
extension (OID 1.3.6.1.4.1.99999.1.1) instead of the spec-compliant
id-alg-mtcProof format. This mode is available for backward compatibility.
make demo-embeddedThis follows the same pattern but embeds the proof in a non-critical X.509
extension rather than the signatureValue field. See the
Embedded Inclusion Proofs section below
for details.
- Standalone ACME server (RFC 8555) runs on a separate port (
:8443) alongside the main service. - JWS verification uses only Go stdlib (ES256/RS256), no external JOSE libraries.
- Account management via JWK thumbprint (RFC 7638).
- Order lifecycle: pending → ready → processing → valid, with http-01 challenge validation (auto-approve for internal CAs).
- CA proxy: Finalize requests are proxied to DigiCert CA REST API; assertion bundles are polled and attached to certificate downloads.
- In-memory nonce store with TTL cleanup.
- Database: 4 new ACME tables, 6 indexes, ~16 CRUD methods.
- Config:
ACMEConfigwith 12 fields, sensible defaults. - Conformance: 6 ACME tests + 3 MTC-spec tests + 3 consistency proof tests (29 total), all passing — including full MTC flow with real CA.
-
Start the Service
make build ./bin/mtc-bridge -config config.yaml
- Main API:
http://localhost:8080 - ACME API:
https://localhost:8443
Or use Docker Compose for reproducible setup:
cp .env.example .env # edit with your CA credentials ./gen-demo-cert.sh # generate self-signed TLS certs docker compose up -d docker compose logs -f acme-server
- Main API:
http://localhost:8080 - ACME API:
https://localhost:8443
- Main API:
-
Check ACME Directory
curl -sk https://localhost:8443/acme/directory | python3 -m json.tool -
Get a Replay-Nonce
curl -skI https://localhost:8443/acme/new-nonce | grep -i replay-nonce -
Run ACME Conformance Tests
./bin/mtc-conformance -url http://localhost:8080 -acme-url https://localhost:8443 -verbose
- All 29 tests should pass, including 6 ACME tests, 3 MTC-spec tests, and 3 consistency proof tests.
Or run all tests with:
make conformance
-
Full ACME Order Flow
- Create account (JWS POST to
/acme/new-account) - Create order (JWS POST to
/acme/new-order) - Get authorization and challenge
- Trigger challenge validation (auto-approved in dev mode)
- Finalize order (proxy CSR to DigiCert CA)
- Download certificate + assertion bundle (PEM)
- See
.ai/phase3-acme-server.mdfor full technical details and API examples.
For a quick demo, use the provided script:
./demo-acme.sh
This automates key/CSR generation, ACME directory/nonce fetch, and guides you to run conformance tests for full flow.
- Create account (JWS POST to
This demo shows how MTC assertion bundles travel inside a TLS handshake. It supports
both MTC-spec certificates and legacy certificates (auto-detected).
The mtc-tls-server staples the Merkle inclusion proof to every TLS connection
via the SignedCertificateTimestamps extension, and mtc-tls-verify connects,
extracts, and cryptographically verifies the proof.
Go's crypto/tls supports a SignedCertificateTimestamps field on
tls.Certificate — byte slices placed here are delivered to the client during
the TLS handshake. We repurpose this CT mechanism to carry the MTC assertion
bundle (JSON-encoded). The client reads it from ConnectionState() and verifies
the Merkle inclusion proof against the bridge's checkpoint.
# 1. Ensure mtc-bridge is running with certificates in the log
make run # or docker compose up -d
# 2. Build Phase 4 binaries
make build
# 3. Start the TLS server (use a cert that's in the bridge's log)
./bin/mtc-tls-server -cert cert.pem -key key.pem -bridge-url http://localhost:8080
# 4. In another terminal, verify the TLS assertion
./bin/mtc-tls-verify -url https://localhost:4443 -insecureOr run the fully automated demo (issues a fresh cert via the CA, waits for the assertion, and runs the verification):
./demo-tls.shMTC TLS Verification Report
===========================
Server: localhost:4443
Subject: CN=tls-demo.meridianfs.com
Serial: AB12CD34...
Leaf Index: 42
Tree Size: 8192
Root Hash: abc123def456...
Proof Depth: 13
Verification:
[PASS] Assertion present in TLS handshake
[PASS] Certificate serial matches assertion
[PASS] Merkle inclusion proof valid
[PASS] Root hash matches checkpoint
[PASS] Certificate not revoked
Result: MTC-VERIFIED
Visit https://localhost:4443/ in a browser to see the MTC status page, or
fetch https://localhost:4443/mtc-status for JSON.
Note: The MTC spec-compliant certificate format (
id-alg-mtcProof) is the recommended approach. This legacy mode uses a custom X.509 extension and is maintained for backward compatibility. See MTC Spec-Compliant Certificate Demo above.
This mode embeds MTC inclusion proofs directly inside X.509 certificates as a custom extension, eliminating the need for separate assertion bundle delivery. This solves the "chicken-and-egg" problem: you need the certificate to compute its Merkle hash, but the proof must be in the certificate.
The approach follows the CT pre-certificate pattern (RFC 6962), adapted for MTC:
-
Phase 1 — Pre-certificate: The local CA issues a valid X.509 certificate from the CSR without the MTC extension. Its TBSCertificate (the "canonical form") is hashed into the Merkle tree as
entry_type=2. -
Immediate checkpoint: The tree is updated and a signed checkpoint is created so the inclusion proof can be computed immediately (no 60-second wait).
-
Phase 2 — Final certificate: The local CA rebuilds the identical template with the MTC inclusion proof extension added, and re-signs. The resulting certificate contains the proof embedded at OID
1.3.6.1.4.1.99999.1.1. -
Verification (offline): Any party can parse the certificate, strip the MTC extension to reconstruct the canonical TBSCertificate, compute the leaf hash, and verify the inclusion proof against the root hash — no network access required.
# 1. Generate a local CA key + certificate (first time only)
make generate-local-ca
# Output: keys/local-ca.key, keys/local-ca.pem
# 2. Enable local CA in config.yaml
# Set local_ca.enabled: true
# Ensure key_file and cert_file paths match
# 3. Start the services
docker compose up -d
# Or: make runlocal_ca:
enabled: true
key_file: "keys/local-ca.key"
cert_file: "keys/local-ca.pem"
validity: 8760h # 1 year default cert validity
organization: "MTC Demo CA"
country: "US"When local_ca.enabled is true, the ACME server uses the local CA for
two-phase signing instead of proxying to DigiCert. Both modes can coexist
(toggle via config). Clients must trust the local CA's root certificate.
# Verify a certificate with an embedded MTC proof
./bin/mtc-verify-cert -cert cert.pem
# Optionally compare against the live checkpoint
./bin/mtc-verify-cert -cert cert.pem -bridge-url http://localhost:8080Example output:
MTC Embedded Proof Verifier
===========================
Subject: example.com
Serial: 5BF2A744...
Issuer: MTC Bridge Local CA
Valid: 2026-03-01 to 2027-03-01
Verification Results:
---------------------
[PASS] MTC inclusion proof extension found (OID 1.3.6.1.4.1.99999.1.1)
Log Origin: http://localhost:8080
Leaf Index: 42
Tree Size: 43
Root Hash: c838a8b9d03f79f8...
Proof Depth: 6 sibling hashes
[PASS] Merkle inclusion proof is valid
All checks passed.
The proof is encoded as an ASN.1 SEQUENCE in a non-critical extension:
OID: 1.3.6.1.4.1.99999.1.1
MTCInclusionProof ::= SEQUENCE {
logOrigin UTF8String, -- bridge URL / log identifier
leafIndex INTEGER, -- position in the Merkle tree
treeSize INTEGER, -- tree size at time of proof
rootHash OCTET STRING(32), -- SHA-256 root hash
proofHashes SET OF OCTET STRING(32), -- sibling hashes
checkpoint UTF8String -- signed checkpoint text
}
./demo-embedded-proof.shThe admin dashboard includes an interactive visualization module at
http://localhost:8080/admin/viz with three viewing modes:
Both views render the certificate hierarchy (CA > Batch Window > Key Algorithm) using HTML5 Canvas. Click segments to drill down, use breadcrumbs to navigate back up.
Color modes control how segments are colored:
- Trust Status — green (valid) vs red (revoked)
- Key Algorithm — purple (post-quantum) vs blue (classical)
- Certificate Age — gradient from green (fresh) to red (expiring)
- Assertion Coverage — green (>80% fresh proofs), amber (>30% stale), red (>50% missing), blue (mixed)
Highlight Revoked toggle dims non-revoked segments and amplifies revoked overlays with stronger colors and borders for quick identification.
The Proof Explorer tab renders the Merkle inclusion proof for any certificate as an interactive binary tree visualization. Enter a leaf index to see:
- The path from leaf to root highlighted in green
- Proof sibling hashes shown in blue at each tree level
- Hover any node to see its full SHA-256 hash
- Side panel with complete proof details (leaf hash, root hash, all proof hashes with left/right indicators)
The visualization uses a cert_metadata cache table that is incrementally
populated by parsing DER certificates on first access. This avoids re-parsing
DER blobs on every request. The table is automatically populated when the
visualization page is loaded.
# Open the visualization
open http://localhost:8080/admin/vizThis section provides step-by-step commands you can run to issue a certificate through the DigiCert Private CA, watch mtc-bridge detect it, verify its inclusion in the Merkle tree, revoke it, and confirm the revocation is tracked.
Note: All commands below assume the DigiCert CA is running on
localhost:80and mtc-bridge is running onlocalhost:8080. Adjust theCA_API_KEY,CA_ID, andTEMPLATE_IDvalues for your environment.
# DigiCert CA API credentials (from your CA provisioning)
export CA_API_KEY="your-api-key-here"
export CA_ID="A76AC522CBABC804919211EB5706CFAD"
export TEMPLATE_ID="0196198F96545084143B237D9E39FC90"
export CA_URL="http://localhost"
export MTC_URL="http://localhost:8080"# View the current checkpoint (tree size + root hash)
curl -s $MTC_URL/checkpointExample output:
localhost/mtc-bridge
7968
yDioudA/efgM/lkppZ5GO87ABRF03/BrdTNk530dq+g=
— mtc-bridge-dev o+eJTz2yzeOM...
The second line (7968) is the number of entries in the tree.
# Generate a key pair and CSR
openssl req -new -newkey rsa:2048 -nodes \
-keyout /tmp/mtc-demo.key \
-subj "/CN=mtc-demo.example.com/O=MTC Demo Corp/C=US" \
-addext "subjectAltName=DNS:mtc-demo.example.com" \
-out /tmp/mtc-demo.csr 2>/dev/null
# Read the CSR and escape newlines for JSON
CSR=$(awk '{printf "%s\\n", $0}' /tmp/mtc-demo.csr)
# Issue the certificate via the DigiCert CA REST API
CERT_RESPONSE=$(curl -s -X POST \
-H "x-api-key: $CA_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{
\"issuer\": {\"id\": \"$CA_ID\"},
\"template_id\": \"$TEMPLATE_ID\",
\"cert_type\": \"private_ssl\",
\"csr\": \"$CSR\",
\"subject\": {
\"common_name\": \"mtc-demo.example.com\",
\"organization_name\": \"MTC Demo Corp\",
\"country\": \"US\"
},
\"validity\": {
\"valid_from\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
\"valid_to\": \"$(date -u -v+365d +%Y-%m-%dT%H:%M:%SZ)\"
},
\"extensions\": {
\"san\": {\"dns_names\": [\"mtc-demo.example.com\"]}
}
}" \
"$CA_URL/certificate-authority/api/v1/certificate")
# Extract the certificate ID and serial number
CERT_ID=$(echo "$CERT_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
SERIAL=$(echo "$CERT_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['serial_number'])")
echo "Certificate ID: $CERT_ID"
echo "Serial Number: $SERIAL"mtc-bridge polls the CA database every 10 seconds and creates a new checkpoint every 60 seconds. Wait about 15-60 seconds, then check:
# Watch the tree size grow
curl -s $MTC_URL/checkpoint
# Or check the admin dashboard
open http://localhost:8080/admin/The tree size should have increased by 1.
# Request an inclusion proof by serial number
curl -s "$MTC_URL/proof/inclusion?serial=$SERIAL" | python3 -m json.toolExample output:
{
"leaf_index": 7968,
"tree_size": 7969,
"leaf_hash": "5b9a1e9e9f15e4ab4d8ddd5faefae9cf...",
"proof": [
"305170f5b9beb10f43d491d0dea2a56d...",
"08f1f04d5d8c81a18d273b8f4f9acaab..."
],
"root_hash": "c838a8b9d03f79f80cfe5929a59e463b...",
"checkpoint": "localhost/mtc-bridge\n7969\n..."
}The response contains:
leaf_index— position of the certificate in the treeproof— the Merkle inclusion proof (list of sibling hashes)root_hash— the tree root that can be independently verifiedcheckpoint— the signed checkpoint anchoring the proof
# Fetch a Merkle hash tile (level 0, tile 0 = first 256 leaves)
curl -s $MTC_URL/tile/0/000 | xxd | head -5
# Fetch an entry bundle tile (first 256 entries)
curl -s $MTC_URL/tile/entries/000 | wc -c
# Fetch the latest checkpoint
curl -s $MTC_URL/checkpoint# Revoke via the DigiCert CA REST API
curl -s -X PUT \
-H "x-api-key: $CA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"revocation_reason": "key_compromise"}' \
"$CA_URL/certificate-authority/api/v1/certificate/$CERT_ID/revocation"
# Response: HTTP 204 No Content (success)
echo "Certificate $CERT_ID revoked."Wait ~30 seconds for the revocation poller to detect the change, then:
# Check the revocation bitfield
# This returns a binary bitfield where each bit represents a tree index.
# A set bit means that index has been revoked.
curl -s $MTC_URL/revocation | wc -c
# Confirm via the admin dashboard
open http://localhost:8080/admin/The admin dashboard will show the updated revocation count.
Assertion bundles are self-contained proof artifacts that package a certificate with its Merkle inclusion proof, signed checkpoint, and parsed X.509 metadata.
# Fetch the assertion bundle by serial number (JSON)
curl -s "$MTC_URL/assertion/$SERIAL" | python3 -m json.toolExample output:
{
"leaf_index": 7968,
"serial_hex": "5BF2A7443A479D5600C6220D369208E325F31C62",
"cert_meta": {
"common_name": "mtc-demo.example.com",
"organization": ["MTC Demo Corp"],
"sans": ["mtc-demo.example.com"],
"key_algorithm": "RSA",
"is_ca": false
},
"leaf_hash": "5b9a1e9e9f15e4ab...",
"proof": ["305170f5b9beb10f...", "08f1f04d5d8c81a1..."],
"tree_size": 7969,
"root_hash": "c838a8b9d03f79f8...",
"checkpoint": "localhost/mtc-bridge\n7969\n...",
"revoked": false,
"log_origin": "localhost/mtc-bridge"
}The bundle includes everything needed to independently verify the certificate's presence in the log — the leaf hash, inclusion proof, tree size, root hash, and signed checkpoint.
# Also available in PEM-like text format
curl -s "$MTC_URL/assertion/$SERIAL/pem"The mtc-assertion CLI tool provides fetch, verify, and inspect
subcommands for working with assertion bundles offline.
# Fetch a bundle by serial number and save to file
./bin/mtc-assertion fetch -serial $SERIAL -output /tmp/bundle.json
# Verify the inclusion proof cryptographically
./bin/mtc-assertion verify -input /tmp/bundle.json
# Output: "Inclusion proof is VALID"
# Pretty-print all bundle details
./bin/mtc-assertion inspect -input /tmp/bundle.jsonverify walks the Merkle inclusion proof from the leaf hash up to the
root, using RFC 9162 hash combining (SHA-256(0x01 ∥ left ∥ right)).
If the computed root matches the checkpoint's root hash, the proof is
valid.
# You can also fetch by tree index
./bin/mtc-assertion fetch -index 1 -output /tmp/bundle1.json
./bin/mtc-assertion inspect -input /tmp/bundle1.json
# Or fetch in PEM format
./bin/mtc-assertion fetch -serial $SERIAL -format pemThe admin dashboard includes a certificate browser with search:
open http://localhost:8080/admin/certsFeatures:
- Search by serial number or cert ID
- Status badges showing Active (green) or Revoked (red)
- Detail pages with parsed X.509 metadata, inclusion proof, and download links for JSON/PEM assertion bundles
The assertion issuer (Phase 2) automatically pre-computes assertion bundles in the background after each checkpoint. Check its progress:
# View aggregate assertion statistics
curl -s $MTC_URL/assertions/stats | python3 -m json.toolExample output:
{
"total_bundles": 500,
"fresh_bundles": 500,
"stale_bundles": 0,
"pending_entries": 7468,
"last_generated": "2026-02-28T13:09:28Z"
}total_bundles— assertion bundles generated so farfresh_bundles— bundles with up-to-date inclusion proofsstale_bundles— bundles needing proof refresh (tree has grown)pending_entries— log entries without a bundle yetlast_generated— timestamp of the last generation cycle
The issuer processes batch_size entries (default: 100) per checkpoint
cycle (~60 seconds). Over time it converges to full coverage of the log.
Downstream consumers (e.g., an ACME server) can poll for newly generated assertion bundles:
# Get bundles generated since checkpoint 0 (all)
curl -s "$MTC_URL/assertions/pending?since=0&limit=5" | python3 -m json.toolExample output:
{
"since": 0,
"count": 5,
"entries": [
{
"entry_idx": 1,
"serial_hex": "5BF2A7443A479D5600C6220D369208E325F31C62",
"checkpoint_id": 42,
"assertion_url": "/assertion/1",
"created_at": "2026-02-28T13:09:28Z"
}
]
}Each entry includes a direct assertion_url to fetch the full bundle.
Use the since parameter as a cursor — track the latest checkpoint_id
you've seen and pass it as since on the next poll.
The admin dashboard includes an Assertion Issuer metrics section:
open http://localhost:8080/admin/The dashboard shows two stats grids:
- Log Statistics — tree size, checkpoints, entries, revocations
- Assertion Issuer — total bundles, fresh/stale/pending counts, last generation time, and cycle duration
These update automatically via HTMX polling.
make conformanceExpected output:
=== MTC tlog-tiles Conformance Test Suite ===
Target: http://localhost:8080
checkpoint_exists [PASS]
checkpoint_format [PASS]
checkpoint_parseable [PASS]
tile_level0_exists [PASS]
tile_hash_size [PASS]
entry_tile_exists [PASS]
entry_tile_parseable [PASS]
inclusion_proof [PASS]
proof_api_inclusion [PASS]
tile_caching [PASS]
revocation_endpoint [PASS]
assertion_bundle_json [PASS]
assertion_bundle_pem [PASS]
assertion_verify_proof [PASS]
assertion_auto_generation [PASS]
assertion_polling [PASS]
assertion_stats [PASS]
acme_directory [PASS]
acme_nonce [PASS]
acme_new_account [PASS]
acme_new_order [PASS]
acme_order_flow [PASS]
acme_full_mtc_flow [PASS]
mtc_cert_format [PASS]
mtc_proof_roundtrip [PASS]
mtc_log_entry_reconstruct [PASS]
consistency_proof_api [PASS]
consistency_proof_verify [PASS]
consistency_proof_edge_cases [PASS]
Results: 29 passed, 0 failed, 0 skipped
The ACME server runs on a separate port (default 8443) and implements RFC 8555 for automated certificate issuance.
export ACME_URL="https://localhost:8443"
# Fetch the ACME directory (-k for self-signed TLS cert)
curl -sk $ACME_URL/acme/directory | python3 -m json.toolExample output:
{
"newAccount": "https://localhost:8443/acme/new-account",
"newNonce": "https://localhost:8443/acme/new-nonce",
"newOrder": "https://localhost:8443/acme/new-order",
"meta": {
"externalAccountRequired": false,
"website": "https://localhost:8443"
}
}# Get a replay-protection nonce
curl -skI $ACME_URL/acme/new-nonce | grep -i replay-nonceThe ACME conformance tests exercise the full order lifecycle:
- Create account — JWS-signed POST with ephemeral ECDSA P-256 key
- Create order — Request certificate for DNS identifiers
- Get authorization — Retrieve http-01 challenge for each identifier
- Trigger validation — POST to challenge URL (auto-approved in dev mode)
- Poll order — Wait for status to transition from
pending→ready - Finalize — Submit CSR; ACME server proxies to DigiCert CA
- Download certificate — Get PEM certificate with appended assertion bundle
Run just the ACME tests:
./bin/mtc-conformance -url http://localhost:8080 -acme-url https://localhost:8443 -verbose 2>&1 | grep acme acme_directory [PASS]
acme_nonce [PASS]
acme_new_account [PASS]
acme_new_order [PASS]
acme_order_flow [PASS]
acme_full_mtc_flow [PASS]
The acme_full_mtc_flow test exercises the complete MTC pipeline: ACME account
creation → order → challenge → CSR finalize (proxied to DigiCert CA) → poll until
the watcher ingests the cert into the Merkle tree → assertion issuer builds the
inclusion proof → certificate download with MTC assertion bundle attached.
| Method | Endpoint | Description |
|---|---|---|
| GET | /checkpoint |
Latest signed checkpoint (C2SP signed-note format) |
| GET | /tile/<L>/<N> |
Merkle hash tile at level L, index N |
| GET | /tile/entries/<N> |
Entry bundle tile at index N |
| GET | /proof/inclusion?serial=<hex>[&index=<n>] |
Inclusion proof for a certificate by serial number |
| GET | /proof/consistency?old=M&new=N |
Consistency proof between two tree sizes (RFC 9162 §2.1.4) |
| GET | /assertion/{query} |
Assertion bundle as JSON (query by index or serial hex) |
| GET | /assertion/{query}/pem |
Assertion bundle in PEM-like text format |
| GET | /assertions/pending?since=<id>&limit=N |
Pre-computed bundles since a checkpoint (polling) |
| GET | /assertions/stats |
Assertion statistics: total, fresh, stale, pending counts |
| GET | /revocation |
Revocation bitfield (binary) |
| GET | /landmarks |
List all landmarks (tree_size → root_hash for signatureless verification) |
| GET | /landmark/{tree_size} |
Specific landmark with subtree signatures |
| GET | /admin/ |
HTMX admin dashboard |
| GET | /admin/certs |
Certificate browser with search |
| GET | /admin/certs/{index} |
Certificate detail page with assertion bundle |
| GET | /admin/viz |
Visualization explorer (Sunburst, Treemap, Proof Explorer) |
| GET | /admin/viz/summary |
Aggregated certificate hierarchy JSON for visualization |
| GET | /admin/viz/certificates |
Paginated leaf-level certificates JSON |
| GET | /admin/viz/revocations |
Revoked entry indices JSON |
| GET | /admin/viz/stats |
Aggregate visualization statistics JSON |
| GET | /admin/viz/proof/{index} |
Merkle inclusion proof tree data for a leaf index |
| GET | /healthz |
Health check |
| Method | Endpoint | Description |
|---|---|---|
| GET | /acme/directory |
ACME directory (RFC 8555 §7.1.1) |
| HEAD/GET | /acme/new-nonce |
Get a fresh anti-replay nonce |
| POST | /acme/new-account |
Create or lookup an account (JWS with JWK) |
| POST | /acme/new-order |
Create a new certificate order |
| POST | /acme/order/{id} |
Get order status |
| POST | /acme/authz/{id} |
Get authorization with challenges |
| POST | /acme/challenge/{id} |
Trigger challenge validation |
| POST | /acme/order/{id}/finalize |
Submit CSR to finalize order |
| POST | /acme/certificate/{id} |
Download certificate + assertion bundle PEM |
<origin>
<tree_size>
<base64 root hash>
— <key_id> <base64 Ed25519 signature>
{
"leaf_index": 42,
"tree_size": 7968,
"leaf_hash": "<hex SHA-256>",
"proof": ["<hex hash>", "..."],
"root_hash": "<hex SHA-256>",
"checkpoint": "<full signed checkpoint text>"
}{
"leaf_index": 42,
"serial_hex": "5BF2A7443A479D5600C6220D369208E325F31C62",
"cert_der": "<base64 DER>",
"cert_meta": {
"common_name": "example.com",
"organization": ["My Org"],
"sans": ["example.com"],
"serial_number": "5BF2A7...",
"not_before": "2026-01-01T00:00:00Z",
"not_after": "2027-01-01T00:00:00Z",
"key_algorithm": "RSA",
"signature_algorithm": "SHA256-RSA",
"is_ca": false
},
"leaf_hash": "<hex SHA-256>",
"proof": ["<hex hash>", "..."],
"tree_size": 7968,
"root_hash": "<hex SHA-256>",
"checkpoint": "<signed checkpoint>",
"revoked": false,
"log_origin": "localhost/mtc-bridge"
}The cert_meta field provides parsed X.509 metadata including subject,
issuer, SANs, key usage, validity period, and more — extracted from the
raw DER certificate without requiring any external parsing tools.
{
"since": 42,
"count": 3,
"entries": [
{
"entry_idx": 100,
"serial_hex": "5BF2A7443A479D5600C6220D369208E325F31C62",
"checkpoint_id": 43,
"assertion_url": "/assertion/100",
"created_at": "2026-02-28T13:09:28Z"
}
]
}Use since as a cursor — pass the latest checkpoint_id you've processed
to get only new bundles. The assertion_url field provides a direct link
to fetch the full bundle.
{
"total_bundles": 500,
"fresh_bundles": 500,
"stale_bundles": 0,
"pending_entries": 7468,
"last_generated": "2026-02-28T13:09:28Z"
}cmd/
mtc-bridge/ Main service binary
mtc-conformance/ Conformance test client (29 tests, including MTC-spec)
mtc-assertion/ CLI tool: fetch, verify, inspect assertion bundles
mtc-tls-server/ Demo TLS server with MTC assertion stapling (MTC-spec + legacy)
mtc-tls-verify/ TLS verification client — auto-detects MTC-spec vs legacy certs
mtc-verify-cert/ Verify certificates offline (MTC-spec id-alg-mtcProof + legacy extension)
demo-embedded-cert/ Standalone demo: generates MTC-spec or legacy certs (--mtc-mode flag)
internal/
acme/ RFC 8555 ACME server (JWS, nonce, accounts, orders, challenges, CA proxy + local CA)
admin/ HTMX dashboard + certificate browser + visualization explorer
assertion/ Assertion bundle builder + JSON/PEM formatter (MTC proof fields)
assertionissuer/ Background assertion generation pipeline + webhooks
batch/ Batch accumulation + multi-cosigner subtree signing + landmarks
cadb/ Read-only MariaDB adapter for DigiCert CA
certutil/ X.509 DER parser for certificate metadata extraction
config/ YAML config with env-var substitution (cosigner algorithms, batch, landmarks)
cosigner/ Ed25519 + ML-DSA key management, checkpoint signing, spec subtree signing
issuancelog/ Entry construction + Merkle tree maintenance + MTC log entries
localca/ Local intermediate CA — MTC-spec certs + legacy two-phase signing
merkle/ RFC 9162 Merkle tree operations + inclusion proofs
mtccert/ MTC certificate construction, parsing, and verification (raw ASN.1)
mtcformat/ MTC wire format: MTCProof, MTCSignature, MerkleTreeCertEntry, TBSCertificateLogEntry
revocation/ Revocation bitfield construction
store/ PostgreSQL state store (subtree_signatures, landmarks, ACME tables)
tlogtiles/ C2SP tlog-tiles HTTP handler + proof + assertion + landmark APIs
watcher/ CA database poller (certs + revocations)
docs/
adr/ Architecture Decision Records (ADR-000 through ADR-008)
design/ System overview documentation
keys/ Ed25519/ML-DSA cosigner keys + local CA key/cert (generated, not committed)
config.yaml Local development configuration
docker-compose.yml Docker Compose for mtc-bridge + PostgreSQL
Dockerfile Multi-stage Docker build (7 binaries)
Makefile Build, test, run, conformance, demo-mtc, demo-embedded targets
See config.yaml for the full configuration reference. Key sections:
state_db— PostgreSQL connection for the Merkle tree stateca_db— MariaDB connection for the DigiCert CA database (read-only)watcher— Polling intervals for certificates and revocationscosigner— Primary cosigner: key file, key ID, algorithm (ed25519,mldsa44,mldsa65,mldsa87), cosigner_idadditional_cosigners— Additional cosigners for multi-cosigner subtree signingbatch— Batch processing: enabled, window duration, min_sizelandmarks— Landmark designation: enabled, intervalassertion_issuer— Assertion generation pipeline (batch size, concurrency, webhooks)acme— ACME server settings (port, CA proxy URL, API key, CA/template IDs, auto-approve)local_ca— Local intermediate CA (key/cert files, validity, org,mtc_modefor spec-compliant certs)http— Listen address, timeouts, cache TTLs
Environment variables can override config values (see docker-compose.yml for
the full list).
# Unit tests (60+ tests across merkle, config, cosigner, certutil, tlogtiles, localca, mtccert, mtcformat packages)
make test
# Conformance tests (29 tests including MTC-spec, requires a running mtc-bridge instance)
make conformance
# Interop validation against bwesterb/mtc reference implementation (standalone, no server needed)
make interop
# Go vet
make vetThe make interop target cross-validates our implementation against the
bwesterb/mtc reference implementation
(Cloudflare's MTC library by Bas Westerbaan). It runs 12 tests:
| Test | What it validates |
|---|---|
| RFC 9162 Leaf Hash | SHA-256(0x00 || data) matches manual computation |
| RFC 9162 Interior Hash | SHA-256(0x01 || left || right) matches manual computation |
| RFC 9162 Tree Root (pow2) | 4-entry tree root matches hand-computed result |
| RFC 9162 Tree Root (non-pow2) | 5-entry tree root with split-point logic |
| RFC 9162 Tree Root (various) | Trees of size 1–257 produce deterministic non-zero roots |
| Inclusion Proof Cross-Validation | Proofs for trees of size 1–100 verify via independent recursive walk |
| Reference CA Roundtrip | Creates a bwesterb/mtc CA (ML-DSA-87), queues 5 assertions, issues a batch |
| Wire Format Null Entry | MerkleTreeCertEntry null encoding roundtrips: [0x00, 0x00] |
| Wire Format TBS Entry | MerkleTreeCertEntry TBS encoding: uint16 BE type + 3-byte BE length + data |
| Wire Format MTCProof | MTCProof marshal/unmarshal with start/end, proof hashes, and signatures |
| Wire Format MTCSignature | Multiple signatures with varying cosigner ID lengths + signatureless mode |
| Reference Tree Auth Path | Builds an 8-leaf tree via bwesterb/mtc's TreeBuilder, verifies all auth paths |
The interop tool lives at cmd/mtc-interop/ and uses github.com/bwesterb/mtc
as a Go dependency. It requires no running server or database.
- bwesterb/mtc (github.com/bwesterb/mtc):
Cloudflare's reference Go implementation. Provides
mtc inspectandmtc verifyCLI tools for validating MTC artifacts. Our interop tests use it as a library. - Cloudflare Azul: Tiled transparency log on Cloudflare Workers, publishing MTCs for live traffic (early 2026 feasibility study with ~1,000 TLS certs).
- Google Chrome: Planning MTC integration in phases — Phase 1 (feasibility study, early 2026), Phase 2 (CT Log operator onboarding, Q1 2027), Phase 3 (Chrome Quantum-resistant Root Store, Q3 2027).
These are the DigiCert Private CA REST API calls used in the walkthrough above.
The API base URL is http://localhost/certificate-authority/api/v1.
POST /certificate-authority/api/v1/certificate
Headers:
x-api-key: <api-key>
Content-Type: application/json
Body:
{
"issuer": {"id": "<CA_ID>"},
"template_id": "<TEMPLATE_ID>",
"cert_type": "private_ssl",
"csr": "<PEM CSR>",
"subject": {
"common_name": "example.com",
"organization_name": "Org",
"country": "US"
},
"validity": {
"valid_from": "2026-01-01T00:00:00Z",
"valid_to": "2027-01-01T00:00:00Z"
},
"extensions": {
"san": {"dns_names": ["example.com"]}
}
}
PUT /certificate-authority/api/v1/certificate/<CERT_ID>/revocation
Headers:
x-api-key: <api-key>
Content-Type: application/json
Body:
{
"revocation_reason": "key_compromise"
}
Response: 204 No Content
GET /certificate-authority/api/v1/certificate?limit=10
Headers:
x-api-key: <api-key>
Copyright (C) 2026 DigiCert, Inc.
This project is licensed under a dual-license model:
You may use, modify, and distribute this software under the terms of the GNU Affero General Public License v3.0.
Key AGPL obligations:
- Source code of any modifications must be made available under the AGPL v3.
- If you run a modified version of this software as a network service, you must make the complete source code available to users of that service (AGPL Section 13).
- Any derivative works must also be licensed under the AGPL v3.
A commercial license is available from DigiCert, Inc. under the DigiCert Master Services Agreement.
The commercial license allows you to:
- Use the software in proprietary products without AGPL obligations
- Distribute modifications without open-source requirements
- Integrate into commercial SaaS offerings
For commercial licensing inquiries, contact sales@digicert.com. See LICENSE_COMMERCIAL.txt for details.
We welcome contributions! Before we can accept your contribution, you must agree to our Contributor License Agreement (CLA).
Why a CLA? The CLA ensures DigiCert can continue to offer this project under both the open-source AGPL v3 and commercial licenses. Your contributions will be available to the entire community under the AGPL v3, and may also be included in DigiCert's commercial offerings.
When you submit a pull request for the first time, the CLA Assistant bot will prompt you to review and sign the CLA electronically. This is a one-time process.
For questions about the CLA, contact opensourcelegal@digicert.com.
This project provides a scriptable, reproducible demo of the complete ACME-to-MTC pipeline: ACME certificate request → DigiCert CA issuance → Merkle tree ingestion → assertion proof generation → certificate download with MTC assertion bundle attached.
- Fill in your secrets and config values in
.env(copy from.env.example) - Ensure Docker and Docker Compose are installed
- DigiCert Private CA and MariaDB available on the Docker network (see docker-compose.yml)
# Required — DigiCert Private CA credentials
CA_API_KEY=your-api-key-here
CA_ID=your-ca-id-here
CA_TEMPLATE_ID=your-template-id-here
CA_URL=http://digicert-ca:8080 # Docker service name
# Database credentials (defaults usually work)
MTC_POSTGRES_PASSWORD=mtcbridge
MTC_CADB_HOST=ca-db
MTC_CADB_PORT=3306
MTC_CADB_USERNAME=<your-db-username>
MTC_CADB_PASSWORD=<your-db-password>
MTC_CADB_DATABASE=digicert_ca- Generate demo TLS certs for the ACME server:
./gen-demo-cert.sh
- Start all services:
docker compose up -d
- Verify everything is healthy:
docker compose ps # all containers should be "Up" curl -s http://localhost:8080/healthz # {"status":"ok"} curl -sk https://localhost:8443/acme/directory | python3 -m json.tool
- Run the conformance suite (includes full MTC flow):
All 29 tests pass, including
make build ./bin/mtc-conformance -url http://localhost:8080 -acme-url https://localhost:8443 -verbose
acme_full_mtc_flowwhich exercises the complete pipeline. - Or run the end-to-end demo script:
./demo-e2e.sh
The Docker Compose setup runs three containers:
- postgres — PostgreSQL 16 state store for the Merkle tree
- mtc-bridge — main service (watcher, tree builder, assertion issuer, HTTP API on :8080)
- acme-server — ACME server (RFC 8555 on :8443 with TLS)
Configuration uses environment variable substitution (${VAR:-default} in config.yaml),
so the same config file works for both local development and Docker deployment. Docker
services communicate via Docker DNS names (e.g., postgres, digicert-ca, ca-db).
With DigiCert CA proxy:
- ACME Account Creation — ES256 JWS-signed request, account stored by JWK thumbprint
- Order + Authorization — DNS identifier order with http-01 challenge (auto-approved for internal CA)
- CSR Finalize — RSA-2048 CSR proxied to DigiCert Private CA REST API
- Certificate Issuance — DigiCert CA issues the certificate, returns serial number
- Merkle Tree Ingestion — Watcher detects the new cert in MariaDB, appends to Merkle tree
- Assertion Proof Generation — Assertion issuer builds inclusion proof bundle at next checkpoint
- Certificate Download — ACME certificate endpoint returns X.509 PEM + MTC assertion bundle
With local CA (MTC-spec mode — recommended):
- ACME Account + Order — same JWS-signed flow
- CSR Finalize — builds
TBSCertificateLogEntrywith SHA-256(SPKI), appends to tree - MTC Certificate —
signatureAlgorithm = id-alg-mtcProof, serial = leaf index - Verification — cosigner signatures (signed) or landmark root hashes (signatureless)
See the script output and admin UI (http://localhost:8080/admin/) for verification.
When local_ca.enabled: true and local_ca.mtc_mode: true in config.yaml,
the ACME server issues spec-compliant MTC certificates:
- ACME Account + Order — same as DigiCert proxy flow
- Finalize — builds TBSCertificateLogEntry (SPKI hashed), appends to tree, computes inclusion proof
- Certificate —
signatureAlgorithm = id-alg-mtcProof,signatureValue = MTCProof - Verification — signed mode (cosigner signatures) or signatureless mode (landmarks)
# Generate local CA (first time)
make generate-local-ca
# Enable in config.yaml:
# local_ca.enabled: true
# local_ca.mtc_mode: true
# Standalone demo (no server needed)
make demo-mtcWhen local_ca.enabled: true without mtc_mode, the ACME server uses two-phase
signing to embed the proof in a custom X.509 extension:
- ACME Account + Order — same as DigiCert proxy flow
- Finalize (two-phase) — local CA issues pre-cert → hashes TBS into tree → re-signs with proof
- Certificate Download — final cert PEM includes the embedded proof extension + CA cert chain
- Offline Verification —
mtc-verify-cert -cert cert.pemverifies the proof without network access
# Standalone demo (no server needed)
make demo-embedded