- Notifications
You must be signed in to change notification settings - Fork2
Go implementation of Merkle Tree Certificates
License
bwesterb/mtc
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
🚨 Merkle Tree Certificates (MTC) is a moving target.
Implementation ofMerkle Tree Certificates for TLSin Go. This contains a Certification Authority (CA), Mirror, andcode to verify certificates.This does not contain integration with TLS (yet) or the ACME bits (yet).At the moment we differ from-04
andmain
branch of the specification,by including someunmerged PRs.
For a proper introduction and motivation, check out thedraft specificationandDavid's TLS working group presentation at IETF116.
Merkle Tree Certificates is anoptimisation to the WebPKI (includingCertificate Transparency)motivated by thelarge sizes oftypical post-quantum signatures and public keys,to reduce the number of keys and signatures required for the common case where
Certificate issuance does not have to be immediate. For instance, becausea certificate can be requested ahead of time for an existing domainby anACME clientlikecertbot.
The relying party (eg. browser) has a trusted update mechanism.There are also several ways to use MTC without trusted update mechanism,with various trade-offs: see theRelying Party Policysection of the specification.
If we're not in this case (which is estimated to beless than 0.1% of the time),then we fall back to regular X.509 certificates.
To play around with MTC, you can install themtc
commandline tool:
$ go install github.com/bwesterb/mtc/cmd/mtc@v0.1.2
In MTC CAs certifyassertions, which bind asubject to aclaim.An informal example of an assertion is:
For TLS, you can trust the P-256 public key
a02342ff2…23ef
when visitingexample.com
or198.51.100.60
.
The first part (TLS and the public key) is thesubject, and thelatter (domain and IP) are theclaim.Roughly, an assertion is like a certificate without the signature.
You can create a request for an assertion to be signed with themtc new-assertion-request
command. First, let's quickly createa P-256 public key to play with.
$ openssl ecparam -name prime256v1 -genkey -out p256.priv$ openssl ec -in p256.priv -pubout -out p256.pub
Now we create an assertion that this P-256 public key shouldbe valid forexample.com
and198.51.100.60
, and write it tothemy-asr
.
$ mtc new-assertion-request --tls-pem p256.pub --dns example.com --ip4 198.51.100.60 -o my-asr
Let's check it usingmtc inspect
:
$ mtc inspect assertion-request my-asrchecksum 2024bdbffe399acca37d299a03c047aa33ef596ae471c17698a0566d00951bd9not_after unsetsubject_type TLSsignature_scheme p256public_key_hash 20b57b9c55dab26db14fb6cc801b7d7294cbf448abb1196e1ffc19d73013498adns [example.com]ip4 [198.51.100.60]evidence-list (0 entries)
An assertion request can contain two bits of extra information besidesthe assertion itself. First is anot_after
field that limitsthe validity of the assertion when published.The second is optional "evidence" that's published alongside theassertions. In the future this could for instance be used forserialized DNSSEC proofs.
We can also create an assertion request derived from an existing X.509certificate at a TLS server using the-X
flag:
$ mtc new-assertion-request -X example.com:443 | mtc inspect assertion-requestchecksum 015d4da06412b4e48f8d93bcbe7bbf43c4684579322cbfbc88d8b653bb2f7e51not_after unsetsubject_type TLSsignature_scheme p256public_key_hash 8d566a5407ab85b413925911c4ce6b13013516006fa8568bf2ec58b9abe04af1dns [example.com]dns_wildcard [example.com]evidence-list (1 entries)umbilical certificate 0 subject CN=*.example.com,O=Internet Corporation for Assigned Names and Numbers,L=Los Angeles,ST=California,C=US issuer CN=DigiCert Global G3 TLS ECC SHA384 2020 CA1,O=DigiCert Inc,C=US serial_no ad893bafa68b0b7fb7a404f06ecaf9a not_before 2025-01-15 00:00:00 +0000 UTC not_after 2026-01-15 23:59:59 +0000 UTC certificate 1 subject CN=DigiCert Global G3 TLS ECC SHA384 2020 CA1,O=DigiCert Inc,C=US issuer CN=DigiCert Global Root G3,OU=www.digicert.com,O=DigiCert Inc,C=US serial_no b00e92d4d6d731fca3059c7cb1e1886 not_before 2021-04-14 00:00:00 +0000 UTC not_after 2031-04-13 23:59:59 +0000 UTC
An MTCA doesn't give you a certificate for an assertion request immediately.Instead, assertions are queued and issued inbatches with a fixed rhythm,for instance a batch is issued once every hour.All assertions in a single batch by default are valid for the same period oftime, thevalidity window, which is, for instance, two weeks.The CA publishes these batches publicly over HTTP.
For each batch, the CA computes aMerkle tree.This condenses all the assertions in that batch into a singletree head hash.For every batch, the CA signs that tree head together with all the tree headsof the currently valid batches. This signature, together with thosesigned tree heads is called thesigned validity window for that batch,which is published alongside the assertions.
Let's create an MTC CA.
$ mtc ca new --batch-duration 5m --lifetime 1h 62253.12.15 ca.example.com/path
This creates a new MTC CA in the current working directory. It's configuredto issue a batch every 5 minutes, and for each batch to be valid for an hour.For a real CA we'd want batch durations in the order of an hour,and a lifetime of a week or two. In this demo we shorten things a bit, sowe don't have to wait too long.
The CA is configured to be hosted athttps://ca.example.com/path
andto be identified by thetrust anchor identifier 62253.12.15.You can get your own by requesting aprivate enterprise number here.
Let's have a look at the files created:
$ find .../signing.key./www./www/mtc./www/mtc/v04b./www/mtc/v04b/ca-params./www/mtc/v04b/batches./queue./tmp
Thesigning.key
file contains the private key of the keypair used by the CA.
Thewww
folder contains the files that have to be servedathttps://ca.example.com/path
. At the moment, the only file of interestisca-params
, which contains the information about the CA:
$ mtc inspect ca-params www/mtc/v04b/ca-paramsissuer 62253.12.15start_time 1745420554 2025-04-23 15:02:34 +0000 UTCbatch_duration 300 5m0slife_time 3600 1h0m0sstorage_window_size 24 2h0m0svalidity_window_size 12server_prefix ca.example.com/pathpublic_key fingerprint ml-dsa-87:84489bcb42b411a85d163ae95e7deb92b106a75840819a985e44d0e01ae3238e
Thebatches
folder is empty, because there are no batches issued yet.
Thequeue
file contains the assertion requests that will be fulfilledduring the next issuance.
Let's issue our first assertion. We can read the assertion request from disk we'vecreated earlier withmtc new-assertion-request
:
$ mtc ca queue -i my-asr $ mtc ca show-queuenot_after 2025-04-23 16:02:33 +0000 UTCsubject_type TLSsignature_scheme p256public_key_hash 20b57b9c55dab26db14fb6cc801b7d7294cbf448abb1196e1ffc19d73013498adns [example.com]ip4 [198.51.100.60]evidence-list (0 entries)Total number of assertion requests in queue: 1
We can also queue an assertion request ad hoc:
$ mtc ca queue --tls-pem p256.pub -d other.example.com -d second.example.com$ mtc ca show-queue | tail -n 10not_after 2025-04-23 16:02:33 +0000 UTCsubject_type TLSsignature_scheme p256public_key_hash 20b57b9c55dab26db14fb6cc801b7d7294cbf448abb1196e1ffc19d73013498adns [example.com]ip4 [198.51.100.60]evidence-list (0 entries)Total number of assertion requests in queue: 2
Let's issue our first batch.
$ mtc ca issue2025/04/23 17:05:20 INFO Starting issuance time=2025-04-23T15:05:20.664Z2025/04/23 17:05:20 INFO Current state expectedStored=0 expectedActive=0 existingBatches=⌀2025/04/23 17:05:20 INFO To issue batches=0
And let's check:
$ find .../signing.key./www./www/mtc./www/mtc/v04b./www/mtc/v04b/ca-params./www/mtc/v04b/batches./www/mtc/v04b/batches/0./www/mtc/v04b/batches/0/validity-window./www/mtc/v04b/batches/0/tree./www/mtc/v04b/batches/0/entries./www/mtc/v04b/batches/0/evidence./www/mtc/v04b/batches/0/index./www/mtc/v04b/batches/latest./queue./tmp
We see batch0
has been created.latest
is a symlink to to0
.
Now, let's have a look at the batch. Theentries
file is essentiallythe list of assertions: the difference between a regular assertionand an entry is that with an entry, the public key has been replacedby the hash of the public key.
$ mtc inspect entries www/mtc/v04b/batches/0/entrieskey 0b65c8a5f69e88fd1eb58dff4d317f6173bd31773e14d99ace88a2aa7062fdd9not_after 2025-04-23 16:02:33 +0000 UTCsubject_type TLSsignature_scheme p256public_key_hash 20b57b9c55dab26db14fb6cc801b7d7294cbf448abb1196e1ffc19d73013498adns [other.example.com second.example.com]key 78b5ccc905b693659bf6581011f8efb17fd7aedf9ca70a196a22923f560feecanot_after 2025-04-23 16:02:33 +0000 UTCsubject_type TLSsignature_scheme p256public_key_hash 20b57b9c55dab26db14fb6cc801b7d7294cbf448abb1196e1ffc19d73013498adns [example.com]ip4 [198.51.100.60]Total number of entries: 2
Thevalidity-window
is the signed validity window: the tree heads ofthe currently valid batches:
$ mtc inspect -ca-params www/mtc/v04b/ca-params validity-window www/mtc/v04b/batches/0/validity-window signature ✅batch_number 0tree_heads[0] 043bc6b0e49a085f2370b2e0f0876d154c2e8d8fe049077dbad118a363580345tree_heads[-1] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-2] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-3] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-4] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-5] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-6] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-7] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-8] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-9] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-10] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-11] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cf
We need to pass theca-params
file to be able to parse the file, andcheck the signature therein. (As not all previous batches exist, they usethe placeholder value for an empty tree.)
Thetree
file contains the Merkle tree.
$ mtc inspect tree www/mtc/v04b/batches/0/tree number of leaves 2number of nodes 3tree head 043bc6b0e49a085f2370b2e0f0876d154c2e8d8fe049077dbad118a363580345
Theevidence
file contains the optional evidence that can be providedwith the assertion request. We did not pass any, so they're empty:
$ mtc inspect evidence www/mtc/v04b/batches/0/evidenceevidence-list (0 entries)evidence-list (0 entries)Total number of evidence lists: 2
Finally, theindex
file allows a quick lookup inentries
(andevidence
)by key (hash of the assertion):
$ mtc inspect index www/mtc/v04b/batches/0/index key seqno offset0b65c8a5f69e88fd1eb58dff4d317f6173bd31773e14d99ace88a2aa7062fdd9 0 0 078b5ccc905b693659bf6581011f8efb17fd7aedf9ca70a196a22923f560feeca 1 91 3total number of entries: 2
As we just issued a new batch, we need to wait a while before thenext batch is ready to issue.
Let's queue some more assertions, wait a bit, and issue a new batch.
$ mtc ca queue --tls-pem p256.pub -d 1.example.com$ mtc ca queue --tls-pem p256.pub -d 2.example.com$ mtc ca queue --tls-pem p256.pub -d 3.example.com$ mtc ca issue2025/04/23 17:12:45 INFO Starting issuance time=2025-04-23T15:12:45.869Z2025/04/23 17:12:45 INFO Current state expectedStored=0,…,2 expectedActive=0,…,2 existingBatches=02025/04/23 17:12:45 INFO To issue batches=1,2$ find .../signing.key./www./www/mtc./www/mtc/v04b./www/mtc/v04b/ca-params./www/mtc/v04b/batches./www/mtc/v04b/batches/0./www/mtc/v04b/batches/0/validity-window./www/mtc/v04b/batches/0/tree./www/mtc/v04b/batches/0/entries./www/mtc/v04b/batches/0/evidence./www/mtc/v04b/batches/0/latest./www/mtc/v04b/batches/0/index./www/mtc/v04b/batches/latest./www/mtc/v04b/batches/1./www/mtc/v04b/batches/1/validity-window./www/mtc/v04b/batches/1/tree./www/mtc/v04b/batches/1/entries./www/mtc/v04b/batches/1/evidence./www/mtc/v04b/batches/1/index./www/mtc/v04b/batches/2./www/mtc/v04b/batches/2/validity-window./www/mtc/v04b/batches/2/tree./www/mtc/v04b/batches/2/entries./www/mtc/v04b/batches/2/evidence./www/mtc/v04b/batches/2/index./queue./tmp
As we waited a bit longer, the current batch is2
, which will containthe queued assertions. The batch1
in between will be empty.Nowlatest
points to2
, and its signed validity window is more interesting.
$ mtc inspect -ca-params www/mtc/v04b/ca-params validity-window www/mtc/v04b/batches/1/validity-windowsignature ✅batch_number 2tree_heads[2] 03a95ba3c354e2b0eb4bea9b111dbc8b97e2c90b85ddcc63d4b635b16f77005dtree_heads[1] 7ceda88ec6c8e34ecacde47588e2605fb86192b94ca96cb897fa6ff442198c8ctree_heads[0] 043bc6b0e49a085f2370b2e0f0876d154c2e8d8fe049077dbad118a363580345tree_heads[-1] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-2] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-3] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-4] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-5] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-6] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-7] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-8] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cftree_heads[-9] a7b081f10c7116c30781a957c3f52625c4d831c8d61ceea021db101ab3c901cf
In MTC, acertificate is an assertion together with a trust anchor identifier(to identify the CA), and an authentication path in the Merkle tree.Let's create one for our initial assertion.
$ mtc ca cert -i my-asr -o my-cert
If we inspect the certificate, it can recompute the root from theauthentication path and CA parameters:
$ mtc inspect -ca-params www/mtc/v04b/ca-params cert my-certsubject_type TLSsignature_scheme p256public_key_hash 20b57b9c55dab26db14fb6cc801b7d7294cbf448abb1196e1ffc19d73013498adns [example.com]ip4 [198.51.100.60]proof_type merkle_tree_sha256CA TAI 62253.12.15Batch number 0index 1recomputed tree head 043bc6b0e49a085f2370b2e0f0876d154c2e8d8fe049077dbad118a363580345authentication path 8964f010faa9e499b21917f8792b541b7b1ac19f313a5d53094c698c2edc330b
This is indeed the root of batch0
, and so this certificate is valid.
To automate this, there is themtc verify
command that takesa certificate, the CA parameters, and a signed validity window.
$ mtc verify -ca-params www/mtc/v04b/ca-params -validity-window www/mtc/v04b/batches/1/validity-window my-cert$ echo $?0
Status code 0 means verification succeeded.
For transparency, you should not get the signed validity window directlyfrom the CA, but rather from one or more mirrors (see below).
An Merkle Tree CA can be run just from the commandline, but it's oftenmore convenient to run it as a server. To start the server, run:
$ mtc ca serve -listen-addr localhost:8080
This will accept HTTP requests onlocalhost:8080
and serve the staticfiles. It will also accept queue requests; periodically issue new batches;and return issued certificates.
Get and inspect CA parameters.
$ curl -s "http://localhost:8080/mtc/v04b/ca-params" -o ca-params$ mtc inspect ca-params ca-paramsissuer 62253.12.15start_time 1745420554 2025-04-23 15:02:34 +0000 UTCbatch_duration 300 5m0slife_time 3600 1h0m0sstorage_window_size 24 2h0m0svalidity_window_size 12server_prefix ca.example.com/pathpublic_key fingerprint ml-dsa-87:84489bcb42b411a85d163ae95e7deb92b106a75840819a985e44d0e01ae3238e
Queue up the assertion created in above.
$ curl -X POST "http://localhost:8080/ca/queue" --data-binary "@my-asr" -w "%{http_code}"200
After it's been issued, we can get the certificate via the/ca/cert
endpoint:
$ curl -X POST "http://localhost:8080/ca/cert" --data-binary "@my-asr" -o my-cert$ mtc inspect -ca-params ca-params cert my-certsubject_type TLSsignature_scheme p256public_key_hash 20b57b9c55dab26db14fb6cc801b7d7294cbf448abb1196e1ffc19d73013498adns [example.com]ip4 [198.51.100.60]proof_type merkle_tree_sha256CA TAI 62253.12.15Batch number 0index 1recomputed tree head 043bc6b0e49a085f2370b2e0f0876d154c2e8d8fe049077dbad118a363580345authentication path 8964f010faa9e499b21917f8792b541b7b1ac19f313a5d53094c698c2edc330b
We can set up a new mirror with themtc mirror new
command:
$ mtc mirror new ca.example.com/path
This will download theca-params
fromhttps://ca.example.com/path/mtc/v04b/ca-params
andset up a directory structure similar to that of a CA:
$ find .../www./www/mtc./www/mtc/v04b./www/mtc/v04b/ca-params./www/mtc/v04b/batches./tmp
To bring the mirror up to date with the CA, use theupdate
command:
$ mtc mirror update2025/04/24 11:54:53 INFO Current state expectedStoredRemote=0 expectedActiveRemote=0 latestRemoteBatch=0 mirroredBatches=⌀2025/04/24 11:54:53 INFO Fetching batch=02025/04/24 11:54:53 INFO Next batch at the earliest in 49s$ find .../www./www/mtc./www/mtc/v04b./www/mtc/v04b/ca-params./www/mtc/v04b/batches./www/mtc/v04b/batches/0./www/mtc/v04b/batches/0/validity-window./www/mtc/v04b/batches/0/tree./www/mtc/v04b/batches/0/entries./www/mtc/v04b/batches/0/evidence./www/mtc/v04b/batches/latest./tmp
To make local testing convenient, when you uselocalhost
as server prefix,the mirror will usehttp
instead ofhttps
. This allows a quick testingset up as follows:
# Set up a CA in the ca folder$ mtc ca -p ca new --batch-duration 5m --lifetime 1h 62253.12.15 localhost:8080$ mtc ca -p ca queue -X example.com:443$ mtc ca -p ca issue$ mtc ca -p ca server -listen-addr localhost:8080 &# Set up a mirror of the CA in the mirror folder$ mtc mirror -p mirror new localhost:8080$ mtc mirror -p mirror update
About
Go implementation of Merkle Tree Certificates
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors4
Uh oh!
There was an error while loading.Please reload this page.