From a570f8fcaa14889d94509d5a9cd60e5de78e6fbb Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 26 May 2026 14:34:27 -0700 Subject: [PATCH 1/4] sa: add schema for MTC operations New tables `checkpoints`, `checkpointSubtrees`, `landmarks`, and some new fields on `orders`. We have a new concept `mtcaID` that is the MTC equivalent of `issuerID` in our pre-existing infrastructure. This is a string-valued field that contains an ASCII-formatted relative OID, to match how CAs are identified in MTC. We also have the corresponding `mirrorID` which contains a relative OID identifying a mirror. Note that we use the more specific term `mirror` rather than `cosigner`, because we expect that root programs will require full mirrors. And since the term "cosigner" in MTC can also refer to a "CA cosigner," we can be clearer by being more specific. Also note that mirror signatures are included as fields rather than as separate rows. That limits us to a fixed number of signatures (one for now), but makes querying simpler. --- sa/db/01-boulder_sa_next.sql | 80 +++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/sa/db/01-boulder_sa_next.sql b/sa/db/01-boulder_sa_next.sql index bd39c37a948..6cbe330bc03 100644 --- a/sa/db/01-boulder_sa_next.sql +++ b/sa/db/01-boulder_sa_next.sql @@ -1,7 +1,7 @@ -- For easy diffability, the main part of this schema -- should be identical with 01-boulder_sa.sql. Any differences -- for the "next" schema should be expressed as ALTER TABLE --- commands at the end of the file. +-- or CREATE TABLE commands at the end of the file. USE boulder_sa_next; CREATE TABLE `authz2` ( @@ -247,3 +247,81 @@ CREATE TABLE `serials` ( ALTER TABLE `certificateStatus` DROP COLUMN `subscriberApproved`; ALTER TABLE `certificateStatus` DROP COLUMN `LockCol`; ALTER TABLE `revokedCertificates` ADD KEY `serial` (`serial`); + +CREATE TABLE `checkpoints` ( + `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 + `mtcaID` varchar(255) NOT NULL, + `mtcaSignature` mediumblob NOT NULL, + -- 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), + `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. + -- Also, for checkpoint signatures start == 0 and end == tree size. + `treeSize` bigint(20) unsigned NOT NULL, + `rootHash` binary(32) NOT NULL, + + `created` datetime DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `mtcaID_treeSize` (`mtcaID`, `treeSize`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; + +-- Represents the 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 + `mtcaID` varchar(255) NOT NULL, + `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), + `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, + + -- Reverse lookup to find a checkpoint given a subtree ID. + `checkpointID` bigint(20) NOT NULL, + + `created` datetime DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `checkpointID` (`checkpointID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; + +CREATE TABLE `landmarks` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `mtcaID` varchar(255) NOT NULL, + `landmarkNumber` bigint(20) unsigned NOT NULL, + `treeSize` bigint(20) unsigned NOT NULL, + `created` datetime DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `mtcaID_landmarkNumber` (`mtcaID`, `landmarkNumber`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; + +ALTER TABLE `orders` + ADD COLUMN `isMTC` bool NOT NULL DEFAULT FALSE, + ADD COLUMN `mtcaID` varchar(255) DEFAULT NULL, + ADD COLUMN `mtcSerialNumber` bigint(20) unsigned DEFAULT NULL, + -- checkpointSubtreeID is a reference to the `checkpointSubtrees` table + ADD COLUMN `checkpointSubtreeID` bigint(20) DEFAULT NULL; From 09fd7ca29dd6bdd28633593dc7b25f63a4006104 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 26 May 2026 15:40:58 -0700 Subject: [PATCH 2/4] Add vschema changes --- sa/vtschema/boulder_sa_next/vschema.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sa/vtschema/boulder_sa_next/vschema.json b/sa/vtschema/boulder_sa_next/vschema.json index 63b8d297095..5069a04b3d1 100644 --- a/sa/vtschema/boulder_sa_next/vschema.json +++ b/sa/vtschema/boulder_sa_next/vschema.json @@ -24,6 +24,9 @@ "registrations": { "column_vindexes": [ { "column": "id", "name": "xxhash" } ] }, "replacementOrders": { "column_vindexes": [ { "column": "serial", "name": "xxhash" } ] }, "revokedCertificates": { "column_vindexes": [ { "column": "serial", "name": "xxhash" } ] }, - "serials": { "column_vindexes": [ { "column": "serial", "name": "xxhash" } ] } + "serials": { "column_vindexes": [ { "column": "serial", "name": "xxhash" } ] }, + "checkpoints": { "column_vindexes": [ { "column": "id", "name": "xxhash" } ] }, + "checkpointSubtrees": { "column_vindexes": [ { "column": "id", "name": "xxhash" } ] }, + "landmarks": { "column_vindexes": [ { "column": "id", "name": "xxhash" } ] } } } From 73f3154f097317485edc9e7dcfbf16c84c1325e9 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 1 Jun 2026 09:59:54 -0700 Subject: [PATCH 3/4] use database name / keyspace name to split logs --- sa/db/00-create-databases.sql | 3 + sa/db/01-boulder_sa_next.sql | 71 ------------------- sa/db/01-mtca.sql | 40 +++++++++++ .../mtcmeta_44947_4_1_0_44/vschema.json | 12 ++++ 4 files changed, 55 insertions(+), 71 deletions(-) create mode 100644 sa/db/01-mtca.sql create mode 100644 sa/vtschema/mtcmeta_44947_4_1_0_44/vschema.json diff --git a/sa/db/00-create-databases.sql b/sa/db/00-create-databases.sql index 69127068a2e..b77bd7f5c1f 100644 --- a/sa/db/00-create-databases.sql +++ b/sa/db/00-create-databases.sql @@ -2,3 +2,6 @@ CREATE DATABASE boulder_sa; CREATE DATABASE incidents_sa; CREATE DATABASE boulder_sa_next; CREATE DATABASE incidents_sa_next; + +-- ISRG(44947) dev-reserved(4) MTCA1(1) logs(0) 44 +CREATE DATABASE mtcmeta_44947_4_1_0_44; diff --git a/sa/db/01-boulder_sa_next.sql b/sa/db/01-boulder_sa_next.sql index 6cbe330bc03..c82fb35320c 100644 --- a/sa/db/01-boulder_sa_next.sql +++ b/sa/db/01-boulder_sa_next.sql @@ -248,77 +248,6 @@ ALTER TABLE `certificateStatus` DROP COLUMN `subscriberApproved`; ALTER TABLE `certificateStatus` DROP COLUMN `LockCol`; ALTER TABLE `revokedCertificates` ADD KEY `serial` (`serial`); -CREATE TABLE `checkpoints` ( - `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 - `mtcaID` varchar(255) NOT NULL, - `mtcaSignature` mediumblob NOT NULL, - -- 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), - `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. - -- Also, for checkpoint signatures start == 0 and end == tree size. - `treeSize` bigint(20) unsigned NOT NULL, - `rootHash` binary(32) NOT NULL, - - `created` datetime DEFAULT current_timestamp(), - PRIMARY KEY (`id`), - KEY `mtcaID_treeSize` (`mtcaID`, `treeSize`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - --- Represents the 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 - `mtcaID` varchar(255) NOT NULL, - `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), - `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, - - -- Reverse lookup to find a checkpoint given a subtree ID. - `checkpointID` bigint(20) NOT NULL, - - `created` datetime DEFAULT current_timestamp(), - PRIMARY KEY (`id`), - KEY `checkpointID` (`checkpointID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - -CREATE TABLE `landmarks` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT, - `mtcaID` varchar(255) NOT NULL, - `landmarkNumber` bigint(20) unsigned NOT NULL, - `treeSize` bigint(20) unsigned NOT NULL, - `created` datetime DEFAULT current_timestamp(), - PRIMARY KEY (`id`), - KEY `mtcaID_landmarkNumber` (`mtcaID`, `landmarkNumber`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - ALTER TABLE `orders` ADD COLUMN `isMTC` bool NOT NULL DEFAULT FALSE, ADD COLUMN `mtcaID` varchar(255) DEFAULT NULL, diff --git a/sa/db/01-mtca.sql b/sa/db/01-mtca.sql new file mode 100644 index 00000000000..857246b2e8d --- /dev/null +++ b/sa/db/01-mtca.sql @@ -0,0 +1,40 @@ +USE mtcmeta_44947_4_1_0_44; + +CREATE TABLE `checkpoints` ( + `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, + `mtcaSignature` mediumblob NOT NULL, + -- 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), + `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 `mtcLogID` and `mirrorID` respectively. + -- Also, for checkpoint signatures start == 0 and end == tree size. + `treeSize` bigint(20) unsigned NOT NULL, + `rootHash` binary(32) NOT NULL, + + `created` datetime DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `mtcLogID_treeSize` (`mtcLogID`, `treeSize`) +) 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 + `mtcLogID` varchar(255) NOT NULL, + `landmarkNumber` bigint(20) unsigned NOT NULL, + `treeSize` bigint(20) unsigned NOT NULL, + `created` datetime DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `mtcLogID_landmarkNumber` (`mtcLogID`, `landmarkNumber`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; + diff --git a/sa/vtschema/mtcmeta_44947_4_1_0_44/vschema.json b/sa/vtschema/mtcmeta_44947_4_1_0_44/vschema.json new file mode 100644 index 00000000000..a7e1d84d71a --- /dev/null +++ b/sa/vtschema/mtcmeta_44947_4_1_0_44/vschema.json @@ -0,0 +1,12 @@ +{ + "sharded":true, + "vindexes": { + "xxhash": { + "type": "xxhash" + } + }, + "tables": { + "checkpoints": { "column_vindexes": [ { "column": "id", "name": "xxhash" } ] }, + "landmarks": { "column_vindexes": [ { "column": "id", "name": "xxhash" } ] } + } +} From 58075240b6ed1aed67a10e6f2d05ba5087786a65 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 1 Jun 2026 16:48:47 -0700 Subject: [PATCH 4/4] review feedback --- sa/db/01-boulder_sa_next.sql | 4 +--- sa/db/01-mtca.sql | 6 ++++++ sa/vtschema/boulder_sa_next/vschema.json | 5 +---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/sa/db/01-boulder_sa_next.sql b/sa/db/01-boulder_sa_next.sql index c82fb35320c..a33ae2fed2b 100644 --- a/sa/db/01-boulder_sa_next.sql +++ b/sa/db/01-boulder_sa_next.sql @@ -251,6 +251,4 @@ ALTER TABLE `revokedCertificates` ADD KEY `serial` (`serial`); ALTER TABLE `orders` ADD COLUMN `isMTC` bool NOT NULL DEFAULT FALSE, ADD COLUMN `mtcaID` varchar(255) DEFAULT NULL, - ADD COLUMN `mtcSerialNumber` bigint(20) unsigned DEFAULT NULL, - -- checkpointSubtreeID is a reference to the `checkpointSubtrees` table - ADD COLUMN `checkpointSubtreeID` bigint(20) DEFAULT NULL; + ADD COLUMN `mtcSerialNumber` bigint(20) unsigned DEFAULT NULL; diff --git a/sa/db/01-mtca.sql b/sa/db/01-mtca.sql index 857246b2e8d..425d4961f09 100644 --- a/sa/db/01-mtca.sql +++ b/sa/db/01-mtca.sql @@ -19,6 +19,9 @@ CREATE TABLE `checkpoints` ( -- 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 `mtcLogID` and `mirrorID` respectively. -- Also, for checkpoint signatures start == 0 and end == tree size. + -- `treeSize` will be strictly increasing over time, enforced by the application. + -- Appending to this table will involve a check (in a transaction) that the treeSize and root hash of the + -- highest-sized checkpoint match what the CA expects. `treeSize` bigint(20) unsigned NOT NULL, `rootHash` binary(32) NOT NULL, @@ -31,6 +34,9 @@ CREATE TABLE `landmarks` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, -- ASCII-format OID relative to 1.3.6.1.4.1 `mtcLogID` varchar(255) NOT NULL, + -- Each newly added landmark must have a strictly larger landmarkNumber + -- than the previous one. We'll enforce this at the application level, + -- not the database level. `landmarkNumber` bigint(20) unsigned NOT NULL, `treeSize` bigint(20) unsigned NOT NULL, `created` datetime DEFAULT current_timestamp(), diff --git a/sa/vtschema/boulder_sa_next/vschema.json b/sa/vtschema/boulder_sa_next/vschema.json index 5069a04b3d1..63b8d297095 100644 --- a/sa/vtschema/boulder_sa_next/vschema.json +++ b/sa/vtschema/boulder_sa_next/vschema.json @@ -24,9 +24,6 @@ "registrations": { "column_vindexes": [ { "column": "id", "name": "xxhash" } ] }, "replacementOrders": { "column_vindexes": [ { "column": "serial", "name": "xxhash" } ] }, "revokedCertificates": { "column_vindexes": [ { "column": "serial", "name": "xxhash" } ] }, - "serials": { "column_vindexes": [ { "column": "serial", "name": "xxhash" } ] }, - "checkpoints": { "column_vindexes": [ { "column": "id", "name": "xxhash" } ] }, - "checkpointSubtrees": { "column_vindexes": [ { "column": "id", "name": "xxhash" } ] }, - "landmarks": { "column_vindexes": [ { "column": "id", "name": "xxhash" } ] } + "serials": { "column_vindexes": [ { "column": "serial", "name": "xxhash" } ] } } }