diff --git a/mtca/mtca.go b/mtca/mtca.go index 52eef80fb66..a2d52617ff0 100644 --- a/mtca/mtca.go +++ b/mtca/mtca.go @@ -18,9 +18,9 @@ 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/borp" "github.com/letsencrypt/boulder/db" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" diff --git a/sa/db/01-boulder_sa_next.sql b/sa/db/01-boulder_sa_next.sql index 8b50a6ce4e7..798b9d6480e 100644 --- a/sa/db/01-boulder_sa_next.sql +++ b/sa/db/01-boulder_sa_next.sql @@ -251,6 +251,8 @@ ALTER TABLE `revokedCertificates` ADD KEY `issuerID_shardIdx_revokedDate_idx` (` 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..8b9a96c0d9c 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(#8991): 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(#8991): 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 diff --git a/trees/treedb/treedb.go b/trees/treedb/treedb.go index 48faeb7cf84..19f30ad74cb 100644 --- a/trees/treedb/treedb.go +++ b/trees/treedb/treedb.go @@ -23,6 +23,8 @@ type CheckpointModel struct { MirrorSignature []byte `db:"mirrorSignature"` TreeSize int64 `db:"treeSize"` RootHash []byte `db:"rootHash"` + SubtreeID1 *int64 `db:"subtreeID1"` + SubtreeID2 *int64 `db:"subtreeID2"` } func (c *CheckpointModel) Valid() error { @@ -54,7 +56,8 @@ func (i *Impl) LatestCheckpoint(ctx context.Context, mtcLogID string) (*Checkpoi latest := new(CheckpointModel) err := i.db.SelectOne(ctx, &latest, `SELECT id, checkpoints.mtcLogID, mtcaSignature, mirrorID, - mirrorSignature, treeSize, rootHash + mirrorSignature, treeSize, rootHash, + subtreeID1, subtreeID2 FROM latestCheckpoint JOIN checkpoints USING(id) WHERE latestCheckpoint.mtcLogID = ? AND