- 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 TLS
At the moment only the Certification Authority (CA) side is implemented,and we differ from-03
andmaster
branch, by includingsomeunmerged 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 the large sizes of typical post-quantum signatures and public keys,to reduce the number of keys and signaturesrequired 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 theTransparency Services sectionof the spec.
MTC does not aim to replace the full WebPKI, but is a first meaningful stepfor improvement.
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.
To create an assertion, you can use themtc new-assertion
command.First, let's quickly create a 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-assertion
.
$ mtc new-assertion --tls-pem p256.pub --dns example.com --ip4 198.51.100.60 -o my-assertionchecksum: 14bc907eafd02d5be8b8cc319d87ad5afe9266a6910a18cbdcbfcee1b7af696a
Let's check it usingmtc inspect
:
$ mtc inspect assertion my-assertionsubject_type TLSsignature_scheme p256public_key_hash a02a1758e4c9d6511dc02f59301b9f29e41762d3d769c87a22333497984a41efdns [example.com]ip4 [198.51.100.60]
An MTC CA doesn't give you a certificate for an assertion immediately. Instead,assertions are queued and issued inbatches with a fixed rhythm,for instance a batch is issused once every hour.All assertions in a single batch are valid for the same period of time,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 singleroot hash.For every batch, the CA signs that root together with all the rootsof the currently valid batches. This signature, together with thosesigned roots 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 calledmy-mtc-ca
, and puts the data in thecurrent working directory. A batch is issued every 5 minutes, andeach batch is valid for one hour.
Let's have a look at the files created:
$ find .../signing.key./www./www/mtc./www/mtc/v1./www/mtc/v1/ca-params./www/mtc/v1/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/v1/ca-paramsissuer 62253.12.15start_time 1705677477 2024-01-19 16:17:57 +0100 CETbatch_duration 300 5m0slife_time 3600 1h0m0sstorage_window_size 24 2h0m0svalidity_window_size 12server_prefix ca.example.com/pathpublic_key fingerprint ml-dsa-87:85b5a617ef109e0a8d68a094c8b969f622ac4096c513fa0acd169c231ce2fad5
Thebatches
folder is empty, because there are no batches issued yet.
Thequeue
file contains the assertions that will be issued.
Let's issue our first assertion. We can read the assertion from disk we'vecreated earlier withmtc new-assertion
:
$ mtc ca queue -i my-assertion $ mtc ca show-queuechecksum 14bc907eafd02d5be8b8cc319d87ad5afe9266a6910a18cbdcbfcee1b7af696asubject_type TLSsignature_scheme p256public_key_hash a02a1758e4c9d6511dc02f59301b9f29e41762d3d769c87a22333497984a41efdns [example.com]ip4 [198.51.100.60]Total number of assertions in queue: 1
(We can pass the checksum fromnew-assertion
with--checksum
to make surethe assertion wasn't corrupted.)
We can also queue an assertion ad hoc:
$ mtc ca queue --tls-pem p256.pub -d other.example.com -d second.example.com$ mtc ca show-queue | tail -n 8checksum fbdea936ae7795a3fa01d44230daba351d7480eceadd086ba32938ebc88a5458subject_type TLSsignature_scheme p256public_key_hash a02a1758e4c9d6511dc02f59301b9f29e41762d3d769c87a22333497984a41efdns [other.example.com second.example.com]Total number of assertions in queue: 2
Let's issue our first batch.
$ mtc ca issue 2024/01/19 16:27:31 INFO Starting issuance time=2024-01-19T16:27:31.841+01:002024/01/19 16:27:31 INFO Current state expectedStored=0 expectedActive=0 existingBatches=⌀2024/01/19 16:27:31 INFO To issue batches=0
And let's check:
$ find .../signing.key./www./www/mtc./www/mtc/v1./www/mtc/v1/ca-params./www/mtc/v1/batches./www/mtc/v1/batches/0./www/mtc/v1/batches/0/tree./www/mtc/v1/batches/0/abridged-assertions./www/mtc/v1/batches/0/signed-validity-window./www/mtc/v1/batches/0/index./www/mtc/v1/batches/latest./queue./tmp
We see a0
batch has been created.latest
is a symlink to0
.
Theabridged-assertions
is essentially the list of assertions:the difference between a regular and abridged assertion,is that with an abridged assertion, the public key has been replacedby the hash of the public key.
$ mtc inspect abridged-assertions www/mtc/v1/batches/0/abridged-assertionskey 28b2216e7905ab48d5444f5b7ebf3d2386bc0444c9721fff77b0b313e734dab4subject_type TLSsignature_scheme p256public_key_hash a02a1758e4c9d6511dc02f59301b9f29e41762d3d769c87a22333497984a41efdns [example.com]ip4 [198.51.100.60]key 80944a1728bc7b4cd7e583c6b24a5f413ba50b7ef5ba9d214e26c1a1974f0a19subject_type TLSsignature_scheme p256public_key_hash a02a1758e4c9d6511dc02f59301b9f29e41762d3d769c87a22333497984a41efdns [other.example.com second.example.com]Total number of abridged assertions: 2
Thesigned-validity-window
is the signed validity window: the roots ofthe currently valid batches:
$ mtc inspect -ca-params www/mtc/v1/ca-params signed-validity-window www/mtc/v1/batches/0/signed-validity-window signature ✅batch_number 0tree_heads[-11] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-10] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-9] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-8] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-7] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-6] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-5] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-4] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-3] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-2] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-1] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[0] c005dcdb53c4e41befcf3a294b815d8b8aa0a260e9f10bfd4e4cb52eb3724aa3
We need to pass theca-params
file to be able to parse the file, andcheck the signature therein. (As this is the first batch, the previous batchescontain a placeholder value.)
Thetree
file contains the Merkle tree.
$ mtc inspect tree www/mtc/v1/batches/0/tree number of leaves 2number of nodes 3root c005dcdb53c4e41befcf3a294b815d8b8aa0a260e9f10bfd4e4cb52eb3724aa3
Finally, theindex
file allows a quick lookup inabridged-assertions
by key (hash of the assertion):
$ mtc inspect index www/mtc/v1/batches/0/index key seqno offset28b2216e7905ab48d5444f5b7ebf3d2386bc0444c9721fff77b0b313e734dab4 0 080944a1728bc7b4cd7e583c6b24a5f413ba50b7ef5ba9d214e26c1a1974f0a19 1 69total 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 issue2024/01/19 16:33:57 INFO Starting issuance time=2024-01-19T16:33:57.860+01:002024/01/19 16:33:57 INFO Current state expectedStored=0,…,2 expectedActive=0,…,2 existingBatches=02024/01/19 16:33:57 INFO To issue batches=1,2$ find .../signing.key./www./www/mtc./www/mtc/v1./www/mtc/v1/ca-params./www/mtc/v1/batches./www/mtc/v1/batches/0./www/mtc/v1/batches/0/tree./www/mtc/v1/batches/0/abridged-assertions./www/mtc/v1/batches/0/signed-validity-window./www/mtc/v1/batches/0/index./www/mtc/v1/batches/latest./www/mtc/v1/batches/1./www/mtc/v1/batches/1/tree./www/mtc/v1/batches/1/abridged-assertions./www/mtc/v1/batches/1/signed-validity-window./www/mtc/v1/batches/1/index./www/mtc/v1/batches/2./www/mtc/v1/batches/2/tree./www/mtc/v1/batches/2/abridged-assertions./www/mtc/v1/batches/2/signed-validity-window./www/mtc/v1/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/v1/ca-params signed-validity-window www/mtc/v1/batches/2/signed-validity-windowsignature ✅batch_number 2tree_heads[-9] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-8] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-7] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-6] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-5] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-4] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-3] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-2] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[-1] f2f65b0486c8cad3876475c9c509afdf3f51dc073b1d2d2d261ff9883d63f98etree_heads[0] c005dcdb53c4e41befcf3a294b815d8b8aa0a260e9f10bfd4e4cb52eb3724aa3tree_heads[1] 98a421741cf06a19b56d7b52436f686885bd798611426f638ffcdb6b5a65c42ctree_heads[2] ab3cb1262fc084be0447c2b3d175d63f6ec2782dcc1443888b12f685976093d5
In MTC, acertificate is an assertion,together with the TrustAnchorIdentifier (consisting of an OID for the CA and the batch number),and an authentication path in the Merkle tree.Let's create one for our initial assertion.
$ mtc ca cert -i my-assertion -o my-cert
If we inspect the certificate, it can recompute the root from the authentication path and CA parameters:
$ mtc inspect -ca-params www/mtc/v1/ca-params cert my-certsubject_type TLSsignature_scheme p256public_key_hash a02a1758e4c9d6511dc02f59301b9f29e41762d3d769c87a22333497984a41efdns [example.com]ip4 [198.51.100.60]proof_type merkle_tree_sha256CA OID 62253.12.15batch 0index 0recomputed root c005dcdb53c4e41befcf3a294b815d8b8aa0a260e9f10bfd4e4cb52eb3724aa3authentication path 00b17df8d909fd3e77005486a16ca00fdc9af38f92a23351359fd420d9f2ef78
This is indeed the root of the0
th batch, and so this certificate is valid.
Run an HTTP server to serve static files, accept queue requests, periodicallyissue new batches of certificate, and serve issued certificates.
Start the server.
$ mtc ca --ca-path . serve --listen-addr localhost:8080
Get and inspect CA parameters.
$ curl -X GET "http://localhost:8080/static/mtc/v1/ca-params" -o ca-params$ mtc inspect ca-params ca-paramsissuer 123.4.5start_time 1739593848 2025-02-14 23:30:48 -0500 ESTbatch_duration 300 5m0slife_time 3600 1h0m0sstorage_window_size 24 2h0m0svalidity_window_size 12server_prefix ca.example.com/pathpublic_key fingerprint ml-dsa-87:be1903a366b462b7b4e0010120d4b38279bbf4e350559b95e93671dbc4b821fc
Queue up the assertion created in above.
$ curl -X POST "http://localhost:8080/ca/queue" --data-binary "@my-assertion" -w "%{http_code}"200
Wait 5 minutes for the next batch and retrieve the certificate from the CA server.
$ curl -X POST "http://localhost:8080/ca/cert" --data-binary "@my-assertion" -o my-cert$ mtc inspect -ca-params ca-params cert my-certsubject_type TLSsignature_scheme p256public_key_hash f3ee2efd8bc3dd01ab924d12ee0bc5661866a4b147fcdc6881b5455c9084c973dns [example.com]ip4 [198.51.100.60]proof_type merkle_tree_sha256CA OID 123.4.5Batch number 991index 0recomputed root 27a893fa1f864f97b2c2073f784bfd6bbcd1b2deb3d8aafbdd18b09ac10bc430authentication path
About
Go implementation of Merkle Tree Certificates