33package mtca
44
55import (
6+ "bytes"
67 "context"
78 "crypto"
8- "crypto/rand"
99 "crypto/sha256"
1010 "crypto/x509"
1111 "database/sql"
1212 "encoding/asn1"
13+ "encoding/base64"
14+ "encoding/hex"
1315 "errors"
1416 "fmt"
1517 "sync"
@@ -26,6 +28,7 @@ import (
2628 mtcapb "github.com/letsencrypt/boulder/mtca/proto"
2729 "github.com/letsencrypt/boulder/trees/cosigned"
2830 "github.com/letsencrypt/boulder/trees/entry"
31+ "github.com/letsencrypt/boulder/trees/tiles"
2932)
3033
3134var ErrIssuanceLogAlreadyInitialized = errors .New ("issuance log already initialized" )
@@ -79,6 +82,11 @@ type mtca struct {
7982 logNumber uint16
8083 pool * pool
8184
85+ // frontier contains all the tiles on the right edge of the tree.
86+ // It will be used to accumulate entries for writing to storage.
87+ // Not safe for concurrent reading and writing.
88+ frontier * tiles.Frontier
89+
8290 sequencingPeriod time.Duration
8391
8492 // TODO: factor our sa.InitWrappedDb() so we get metrics and other goodies.
@@ -122,7 +130,17 @@ func initDB(dbMap *borp.DbMap) *db.WrappedMap {
122130// InitLog creates the database metadata for a new, empty log: one checkpoint and the row
123131// in `latestCheckpoint` that refers to it. Should only be run once in a log's lifetime.
124132func (m * mtca ) InitLog (ctx context.Context ) error {
125- _ , err := db .WithTransaction (ctx , m .db , func (tx db.Executor ) (any , error ) {
133+ candidate := & tiles.Frontier {}
134+
135+ nullEntry := & entry.MTCLogEntry {}
136+ err := candidate .AppendEntry (nullEntry )
137+ if err != nil {
138+ return err
139+ }
140+
141+ rootHash := candidate .RootHash ()
142+
143+ _ , err = db .WithTransaction (ctx , m .db , func (tx db.Executor ) (any , error ) {
126144 var numLatestCheckpoints int64
127145 err := tx .SelectOne (ctx , & numLatestCheckpoints , "SELECT COUNT(*) FROM latestCheckpoint WHERE mtcLogID = ?" ,
128146 m .mtcLogID ())
@@ -146,19 +164,9 @@ func (m *mtca) InitLog(ctx context.Context) error {
146164 m .mtcLogID (), numCheckpoints , numLatestCheckpoints )
147165 }
148166
149- // null_entry has empty extensions and a MTCLogEntryType of 0. Since extensions can be up to 2^16 long
150- // there's two bytes of length prefix. Since MTCLogEntryType can have up to 2^16 values, it's also two bytes.
151- // All the bytes are zero: empty extensions, null_entry type is enum value zero.
152- // https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#name-log-entries
153- // To calculate the Merkle Tree Hash of a single-entry list, we prepend 0x00 (as compared with 0x01 when hashing
154- // two nodes). So five zeroes total.
155- // https://www.rfc-editor.org/info/rfc9162/#name-definition-of-the-merkle-tr
156- nullEntry := []byte {0 , 0 , 0 , 0 , 0 }
157- rootHash := sha256 .Sum256 (nullEntry )
158-
159167 firstCheckpoint := checkpoint {
160168 MTCLogID : m .mtcLogID (),
161- TreeSize : 1 ,
169+ TreeSize : candidate . TreeSize () ,
162170 RootHash : rootHash [:],
163171 }
164172
@@ -188,9 +196,24 @@ func (m *mtca) InitLog(ctx context.Context) error {
188196 return nil , nil
189197 })
190198 if err != nil {
199+ if errors .Is (err , ErrIssuanceLogAlreadyInitialized ) {
200+ // The DB thinks the log is initialized; make sure the tiles are there.
201+ err2 := m .Preflight (ctx )
202+ if err2 != nil {
203+ return fmt .Errorf ("DB is initialized but Preflight returns: %s" , err2 )
204+ }
205+ return err
206+ }
191207 return err
192208 }
193209
210+ err = candidate .Publish (ctx , m .s3c , m .tileStoragePrefix ())
211+ if err != nil {
212+ return err
213+ }
214+
215+ m .frontier = candidate
216+
194217 _ , err = m .latestCheckpoint (ctx )
195218 if err != nil {
196219 return fmt .Errorf ("fetching first checkpoint: %s" , err )
@@ -199,13 +222,37 @@ func (m *mtca) InitLog(ctx context.Context) error {
199222 return err
200223}
201224
225+ // Preflight gets the latest checkpoint from the database and reads the corresponding
226+ // frontier tiles from storage. It must be called on startup, before Loop().
227+ func (m * mtca ) Preflight (ctx context.Context ) error {
228+ latest , err := m .latestCheckpoint (ctx )
229+ if err != nil {
230+ return err
231+ }
232+ frontier , err := tiles .LoadFrontier (ctx , m .s3c , latest .TreeSize , m .tileStoragePrefix ())
233+ if err != nil {
234+ return err
235+ }
236+
237+ tileBasedHash := frontier .RootHash ()
238+ if ! bytes .Equal (latest .RootHash , tileBasedHash [:]) {
239+ return fmt .Errorf ("state mismatch: at tree size %d, DB contains RootHash %s, but frontier tiles calculate %s" ,
240+ latest .TreeSize ,
241+ base64 .StdEncoding .EncodeToString (latest .RootHash [:]),
242+ tileBasedHash )
243+ }
244+
245+ m .frontier = frontier
246+ return nil
247+ }
248+
202249type pool struct {
203250 sync.RWMutex
204251 entries []pendingEntry
205252 maxSize int
206253}
207254
208- // pendingEntry represents an pending entry in the pool, along with a channel to notify a pending RPC.
255+ // pendingEntry represents a pending entry in the pool, along with a channel to notify a pending RPC.
209256type pendingEntry struct {
210257 mtcle * entry.MTCLogEntry
211258 ch chan <- int64
@@ -242,8 +289,29 @@ func (m *mtca) mtcLogID() string {
242289 return fmt .Sprintf ("%s.0.%d" , m .mtcaID , m .logNumber )
243290}
244291
292+ // tileStoragePrefix returns the path within a bucket where we will store tiles.
293+ //
294+ // https://github.com/C2SP/C2SP/blob/main/mtc-tlog.md#serving-issuance-logs
295+ //
296+ // "Each log's prefix URL is the concatenation of the CA prefix URL and the log number, encoded as an
297+ // ASCII decimal integer with no additional leading zeros:
298+ //
299+ // <CA prefix URL>/<log number>"
300+ //
301+ // We assume we will serve directly from tile storage (likely sync'ed somewhere), so we
302+ // want to store tiles compatible with that pattern, with log number as the last component
303+ // of the path before "tile/".
304+ //
305+ // As a matter of local convention we will put the MTCA ID as the path component right before
306+ // log number.
307+ func (m * mtca ) tileStoragePrefix () string {
308+ return fmt .Sprintf ("%s/%d" , m .mtcaID , m .logNumber )
309+ }
310+
245311// Issue requests a TBSCertificateLogEntry be issued and returns after it's been sequenced into the log
246312// and a new checkpoint signed by the CA. It does not wait for a mirror cosignature.
313+ //
314+ // Safe for concurrent calls. Implements a gRPC method.
247315func (m * mtca ) Issue (ctx context.Context , req * mtcapb.IssueRequest ) (* mtcapb.IssueResponse , error ) {
248316 key , err := x509 .ParsePKIXPublicKey (req .Pubkey )
249317 if err != nil {
@@ -279,7 +347,7 @@ func (m *mtca) Issue(ctx context.Context, req *mtcapb.IssueRequest) (*mtcapb.Iss
279347
280348 mtcle , err := entry .FromX509 (lintCertBytes , crypto .SHA256 )
281349 if err != nil {
282- return nil , fmt .Errorf ("generating MTCLogEntry: %s '%x' " , err , lintCertBytes )
350+ return nil , fmt .Errorf ("generating MTCLogEntry: %s" , err )
283351 }
284352
285353 // We'll get notification of sequencing on this channel. Buffer it so `sequence()` doesn't
@@ -309,6 +377,8 @@ func (m *mtca) Issue(ctx context.Context, req *mtcapb.IssueRequest) (*mtcapb.Iss
309377
310378// Loop periodically sequences all entries in the pool and sends notifications to the waiting RPCs.
311379//
380+ // Must be called after Preflight() returns success.
381+ //
312382// At process shutdown, this context should be canceled _after_ GracefulStop returns. That ensures
313383// there are no inflight RPCs from clients, which in turn ensures that we have sequenced everything
314384// had in the pool.
@@ -385,7 +455,13 @@ func (m *mtca) fakePublisher(ctx context.Context) {
385455//
386456// Each entry in the pool will get a notification on its channel: either the index at which
387457// it was sequenced, or -1 if there was an error during sequencing.
458+ //
459+ // Must only be called after Preflight() returns success.
388460func (m * mtca ) sequence (ctx context.Context ) error {
461+ if m .frontier == nil {
462+ return fmt .Errorf ("call mtca.Preflight() before sequencing" )
463+ }
464+
389465 if m .pool .len () == 0 {
390466 return nil
391467 }
@@ -414,26 +490,44 @@ func (m *mtca) sequence(ctx context.Context) error {
414490 }
415491 }()
416492
417- // Simulate writing to tile storage
418- latestTreeSize := latest . TreeSize
419- var entryIndexes [] int64
493+ candidate := m . frontier . Clone ()
494+
495+ // Add leaves to the candidate.
420496 for _ , e := range entries {
421- m .log .AuditInfo ("Preparing to issue: %x" , e )
422- entryIndexes = append (entryIndexes , latestTreeSize )
423- latestTreeSize ++
497+ err = candidate .AppendEntry (e .mtcle )
498+ if err != nil {
499+ return err
500+ }
424501 }
425502
426- // TODO: calculate new root hash for real
427- var newRootHash [sha256 .Size ]byte
428- rand .Read (newRootHash [:])
503+ newRootHash := candidate .RootHash ()
504+
505+ // Log each leaf along with the root hash it will be included in.
506+ for i , e := range entries {
507+ m .log .AuditInfo ("issuing" , map [string ]any {
508+ "TBSCertificateLogEntry" : hex .EncodeToString (e .mtcle .TBS ()),
509+ "entryIndex" : latest .TreeSize + int64 (i ),
510+ "mtcLogID" : m .mtcLogID (),
511+ "newRootHash" : newRootHash .String (),
512+ })
513+ }
514+
515+ // First stage to a pending area. After signing and storing to the DB
516+ // (but before publishing a new checkpoint signed note), we will flush
517+ // to the live location. This ensures we've persisted the tiles before
518+ // committing to a tree hash by signing it.
519+ err = candidate .Stage (ctx , m .s3c , m .tileStoragePrefix ())
520+ if err != nil {
521+ return fmt .Errorf ("staging candidate tiles: %s" , err )
522+ }
429523
430524 newCheckpoint := checkpoint {
431525 ID : 0 ,
432526 MTCLogID : m .mtcLogID (),
433527 MTCASignature : nil ,
434528 MirrorID : "" ,
435529 MirrorSignature : nil ,
436- TreeSize : latestTreeSize ,
530+ TreeSize : candidate . TreeSize () ,
437531 RootHash : newRootHash [:],
438532 }
439533
@@ -510,9 +604,24 @@ func (m *mtca) sequence(ctx context.Context) error {
510604 return err
511605 }
512606
607+ m .frontier = candidate
608+
609+ // Write the tiles to a live serving location.
610+ //
611+ // TODO(#8902): This should include indefinite retries on error. We've committed to the
612+ // tree hash by signing it, so nothing can make progress until we've published the tiles.
613+ //
614+ // Once we add publishing of checkpoints as signed notes, publication of the signed note
615+ // should come after this flush succeeds, so monitors don't try to fetch tiles that aren't
616+ // yet available.
617+ err = m .frontier .Publish (ctx , m .s3c , m .tileStoragePrefix ())
618+ if err != nil {
619+ return fmt .Errorf ("publishing tiles: %s" , err )
620+ }
621+
513622 // Notify waiting RPCs.
514623 for i , e := range entries {
515- e .ch <- entryIndexes [ i ]
624+ e .ch <- latest . TreeSize + int64 ( i )
516625 }
517626 // Empty out the entries list so the deferred error path doesn't try to notify them.
518627 entries = nil
0 commit comments