From a33504048caaa22386e85991d18d5f6d3e969acb Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 28 Aug 2026 13:55:06 -0700 Subject: [PATCH 1/2] db: add checkpointSubtrees table To generate a standalone certificate, we need to map an order / MTC serial number to a subtree that (a) contains that serial number and (b) will eventually get a mirror signature written to it by the mtpublisher. Add a new table to store (up to) two subtrees per checkpoint, covering the entries between that checkpoint and the previous one. Also add an `mtcSubtreeID` field to order objects. Adds more documentation about what's stored in the `checkpoints.mirrorSignature` and `checkpoints.mtcaSignature` fields, with a TODO to change what's stored there. --- mtca/mtca.go | 4 ++- sa/db/01-boulder_sa_next.sql | 4 ++- sa/db/01-mtcmeta_44947_4_1_0_44.sql | 53 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/mtca/mtca.go b/mtca/mtca.go index d41c248e37a..41d4f3f4bac 100644 --- a/mtca/mtca.go +++ b/mtca/mtca.go @@ -21,7 +21,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/jmhodges/clock" "github.com/letsencrypt/borp" - "golang.org/x/mod/sumdb/tlog" "github.com/letsencrypt/boulder/db" @@ -602,6 +601,8 @@ type checkpoint struct { MirrorSignature []byte `db:"mirrorSignature"` TreeSize int64 `db:"treeSize"` RootHash []byte `db:"rootHash"` + SubtreeID1 *int64 `db:"subtreeID1"` + SubtreeID2 *int64 `db:"subtreeID2"` } func (c *checkpoint) valid() error { @@ -648,6 +649,7 @@ func (m *mtca) latestCheckpoint(ctx context.Context) (*checkpoint, error) { err := m.db.SelectOne(ctx, &latest, `SELECT id, checkpoints.mtcLogID, mtcaSignature, mirrorID, mirrorSignature, treeSize, rootHash + subtreeID1, subtreeID2, FROM latestCheckpoint JOIN checkpoints USING(id) WHERE latestCheckpoint.mtcLogID = ? AND diff --git a/sa/db/01-boulder_sa_next.sql b/sa/db/01-boulder_sa_next.sql index 934175e3d19..6e76fd937b7 100644 --- a/sa/db/01-boulder_sa_next.sql +++ b/sa/db/01-boulder_sa_next.sql @@ -250,6 +250,8 @@ ALTER TABLE `revokedCertificates` ADD KEY `serial` (`serial`); ALTER TABLE `orders` ADD COLUMN `mtcLogID` varchar(255) DEFAULT NULL, - ADD COLUMN `mtcSerialNumber` bigint(20) unsigned DEFAULT NULL; + ADD COLUMN `mtcSerialNumber` bigint(20) unsigned DEFAULT NULL, + -- A reference to the `subtreeCheckpoints` table. + ADD COLUMN `mtcSubtreeID` bigint(20) unsigned DEFAULT NULL; ALTER TABLE `authz2` ADD COLUMN `beganProcessing` tinyint(1) NOT NULL DEFAULT 0; diff --git a/sa/db/01-mtcmeta_44947_4_1_0_44.sql b/sa/db/01-mtcmeta_44947_4_1_0_44.sql index 58639a01925..63c3e918c02 100644 --- a/sa/db/01-mtcmeta_44947_4_1_0_44.sql +++ b/sa/db/01-mtcmeta_44947_4_1_0_44.sql @@ -26,12 +26,19 @@ CREATE TABLE `checkpoints` ( -- This is redundant with the database/keyspace name and will be used for extra checks to ensure -- configuration errors can't result in using the wrong database/keyspace. `mtcLogID` varchar(255) NOT NULL, + -- An ML-DSA-44 signature over a CosignedMessage with `timestamp` and `start` both zero. + -- TODO: Change this to store a signed note signature line instead, since that format can + -- carry a nonzero `timestamp`. `mtcaSignature` mediumblob, + -- For simplicity we start out with a hardcoded assumption of one mirror signature, -- the planned CQRP requirement. If requirements increase we can add more fields. -- `mirrorID` is an ASCII-format OID relative to 1.3.6.1.4.1. -- Note: these two fields start empty and are filled later. `mirrorID` varchar(255), + -- An ML-DSA-44 signature over a CosignedMessage with `timestamp` and `start` both zero. + -- TODO: Change this to store a signed note signature line instead, since that format can + -- carry a nonzero `timestamp`. `mirrorSignature` mediumblob, -- Signed-over data: https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#section-5.3.1 @@ -41,11 +48,57 @@ CREATE TABLE `checkpoints` ( `treeSize` bigint(20) unsigned NOT NULL, `rootHash` binary(32) NOT NULL, + -- The IDs of the one or two subtrees in the `checkpointSubtrees` table implied by the delta between + -- this checkpoint and the previous one. These two subtrees must cover all entries between this checkpoint + -- and the previous one. + -- The first checkpoint of an issuance log will have these both NULL. Subsequent checkpoints will have `subtreeID2` NULL + -- if a single subtree covers the delta. + `subtreeID1` bigint(20) unsigned, + `subtreeID2` bigint(20) unsigned, + `created` datetime DEFAULT current_timestamp(), PRIMARY KEY (`id`), KEY `mtcLogID_treeSize` (`mtcLogID`, `treeSize`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +-- Represents the pair-of-subtrees signed every checkpoint, to put into standalone certificates. +-- The subtrees used for landmark-relative certificates are not represented explicitly +-- in the database. +-- +-- Note: standalone certificates are assigned to a specific checkpoint subtree at issuance. +-- We don't look these up dynamically by serial number, so we don't have indexes on +-- `subtreeStart` or `subtreeEnd`. Note that if we do later add an index, we'd have to pick +-- either `subtreeStart` or `subtreeEnd`, since indexes are hierarchical. An index on +-- (`subtreeStart`, `subtreeEnd`) isn't better than an index on (`subtreeEnd`) for answering +-- the question "which checkpoint subtrees include N?". +CREATE TABLE `checkpointSubtrees` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + -- ASCII-format OID relative to 1.3.6.1.4.1 + -- https://tlswg.org/tls-trust-anchor-ids/draft-ietf-tls-trust-anchor-ids.html#name-trust-anchor-identifiers + -- https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#ca-ids + -- This is redundant with the database/keyspace name and will be used for extra checks to ensure + -- configuration errors can't result in using the wrong database/keyspace. + `mtcLogID` varchar(255) NOT NULL, + -- An ML-DSA-44 signature over a CosignedMessage with zero timestamp. + `mtcaSignature` mediumblob NOT NULL, + + -- `mirrorID` is an ASCII-format OID relative to 1.3.6.1.4.1. + -- Note: these two fields start empty and are filled later. + `mirrorID` varchar(255), + -- An ML-DSA-44 signature over a CosignedMessage with zero timestamp. + `mirrorSignature` mediumblob, + + -- Signed-over data: https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#section-5.3.1 + -- Note that `log_origin` and `cosigner_name` in the link above are derived from `mtcaID` and `mirrorID` respectively. + `subtreeStart` bigint(20) unsigned NOT NULL, + `subtreeEnd` bigint(20) unsigned NOT NULL, + `subtreeHash` binary(32) NOT NULL, + + `created` datetime DEFAULT current_timestamp(), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; + + CREATE TABLE `landmarks` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, -- ASCII-format OID relative to 1.3.6.1.4.1 From 35016470a346f5c2459553efc73c996447891090 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 28 Aug 2026 14:26:31 -0700 Subject: [PATCH 2/2] Updates --- mtca/mtca.go | 4 ++-- sa/db/01-mtcmeta_44947_4_1_0_44.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mtca/mtca.go b/mtca/mtca.go index 41d4f3f4bac..a843281ae9f 100644 --- a/mtca/mtca.go +++ b/mtca/mtca.go @@ -648,8 +648,8 @@ func (m *mtca) latestCheckpoint(ctx context.Context) (*checkpoint, error) { var latest checkpoint err := m.db.SelectOne(ctx, &latest, `SELECT id, checkpoints.mtcLogID, mtcaSignature, mirrorID, - mirrorSignature, treeSize, rootHash - subtreeID1, subtreeID2, + mirrorSignature, treeSize, rootHash, + subtreeID1, subtreeID2 FROM latestCheckpoint JOIN checkpoints USING(id) WHERE latestCheckpoint.mtcLogID = ? AND diff --git a/sa/db/01-mtcmeta_44947_4_1_0_44.sql b/sa/db/01-mtcmeta_44947_4_1_0_44.sql index 63c3e918c02..8b9a96c0d9c 100644 --- a/sa/db/01-mtcmeta_44947_4_1_0_44.sql +++ b/sa/db/01-mtcmeta_44947_4_1_0_44.sql @@ -27,7 +27,7 @@ CREATE TABLE `checkpoints` ( -- configuration errors can't result in using the wrong database/keyspace. `mtcLogID` varchar(255) NOT NULL, -- An ML-DSA-44 signature over a CosignedMessage with `timestamp` and `start` both zero. - -- TODO: Change this to store a signed note signature line instead, since that format can + -- TODO(#8991): Change this to store a signed note signature line instead, since that format can -- carry a nonzero `timestamp`. `mtcaSignature` mediumblob, @@ -37,7 +37,7 @@ CREATE TABLE `checkpoints` ( -- Note: these two fields start empty and are filled later. `mirrorID` varchar(255), -- An ML-DSA-44 signature over a CosignedMessage with `timestamp` and `start` both zero. - -- TODO: Change this to store a signed note signature line instead, since that format can + -- TODO(#8991): Change this to store a signed note signature line instead, since that format can -- carry a nonzero `timestamp`. `mirrorSignature` mediumblob,