-
-
Notifications
You must be signed in to change notification settings - Fork 646
mtca: Serve CQRP Landmark Service compatible checkpoints #8974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ import ( | |
| "github.com/letsencrypt/boulder/issuance" | ||
| blog "github.com/letsencrypt/boulder/log" | ||
| mtcapb "github.com/letsencrypt/boulder/mtca/proto" | ||
| "github.com/letsencrypt/boulder/trees/checkpoint" | ||
| "github.com/letsencrypt/boulder/trees/cosignature" | ||
| "github.com/letsencrypt/boulder/trees/entry" | ||
| "github.com/letsencrypt/boulder/trees/issuancelog" | ||
|
|
@@ -47,6 +48,8 @@ func New( | |
| sequencingPeriod time.Duration, | ||
| dbMap *borp.DbMap, | ||
| s3c simpleS3, | ||
| mirrorID string, | ||
| mirrorPublicKey *mldsa.PublicKey, | ||
| logger blog.Logger, | ||
| clk clock.Clock, | ||
| ) (*mtca, error) { | ||
|
|
@@ -66,12 +69,14 @@ func New( | |
| issuer: issuer, | ||
| profiles: profiles, | ||
| logID: logID, | ||
| mirrorID: mirrorID, | ||
| pool: &pool{maxSize: 100}, | ||
|
|
||
| sequencingPeriod: sequencingPeriod, | ||
|
|
||
| db: initDB(dbMap), | ||
| s3c: s3c, | ||
|
|
||
| log: logger, | ||
| clk: clk, | ||
| } | ||
|
|
@@ -92,6 +97,12 @@ func New( | |
| } | ||
| m.verifier = verifier | ||
|
|
||
| mirrorVerifier, err := cosignature.NewVerifier(mirrorID, mirrorPublicKey) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("creating mirror verifier: %s", err) | ||
| } | ||
| m.mirrorVerifier = mirrorVerifier | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Build this earlier, so it can be populated when mtca is first built on line 68, rather than assigning after the fact. |
||
|
|
||
| return m, nil | ||
| } | ||
|
|
||
|
|
@@ -104,6 +115,11 @@ type mtca struct { | |
| cosigner *cosignature.Cosigner | ||
| verifier *cosignature.Verifier | ||
|
|
||
| mirrorID string | ||
| mirrorVerifier *cosignature.Verifier | ||
|
|
||
| servedCheckpointID int64 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO, .New() should read from S3 to populate this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally |
||
|
|
||
| pool *pool | ||
|
|
||
| // frontier contains all the tiles on the right edge of the tree. | ||
|
|
@@ -407,9 +423,10 @@ func (m *mtca) Loop(ctx context.Context) { | |
| } | ||
|
|
||
| // sequence takes all entries from the pool, simulates writing them to tile storage, signs | ||
| // and stores a new checkpoint, and notifies waiting RPCs. | ||
| // and stores a new checkpoint, and notifies waiting RPCs. It also serves the latest | ||
| // checkpoint's cosigned note once the mirror signature arrives, when the pool is empty. | ||
| // | ||
| // If the pool is empty, nothing happens. | ||
| // If the pool is empty, no sequencing happens. | ||
| // If the pool is non-empty, but the previous checkpoint doesn't have a mirror signature, | ||
| // returns an error that wraps ErrCheckpointNotReady (without taking entries from the pool). | ||
| // This is expected to be a common occurrence. | ||
|
|
@@ -423,15 +440,22 @@ func (m *mtca) sequence(ctx context.Context) error { | |
| return fmt.Errorf("call mtca.Preflight() before sequencing") | ||
| } | ||
|
|
||
| if m.pool.len() == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| latest, err := m.latestCheckpoint(ctx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if latest.mirrored() { | ||
| err = m.serveCheckpoint(ctx, latest) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| if m.pool.len() == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| if !latest.mirrored() { | ||
| return fmt.Errorf("temporary: checkpoint ID %d (tree size %d): %w", | ||
| latest.ID, latest.TreeSize, ErrCheckpointNotReady) | ||
|
|
@@ -571,10 +595,6 @@ func (m *mtca) sequence(ctx context.Context) error { | |
| // | ||
| // TODO(#8902): This should include indefinite retries on error. We've committed to the | ||
| // tree hash by signing it, so nothing can make progress until we've published the tiles. | ||
| // | ||
| // Once we add publishing of checkpoints as signed notes, publication of the signed note | ||
| // should come after this flush succeeds, so monitors don't try to fetch tiles that aren't | ||
| // yet available. | ||
| err = m.frontier.Publish(ctx, m.s3c, m.logID.TilePrefix()) | ||
| if err != nil { | ||
| return fmt.Errorf("publishing tiles: %s", err) | ||
|
|
@@ -590,7 +610,7 @@ func (m *mtca) sequence(ctx context.Context) error { | |
| return nil | ||
| } | ||
|
|
||
| // checkpoint represents the database storage of a checkpoint and associated signatures. | ||
| // checkpointRow represents the database storage of a checkpoint and associated signatures. | ||
| // | ||
| // For signing, the TreeSize and RootHash fields are incorporated into a `cosigned.Message`. | ||
| type checkpointRow struct { | ||
|
|
@@ -683,3 +703,49 @@ func (m *mtca) signCheckpoint(c *checkpointRow) ([]byte, error) { | |
| } | ||
| return cosignature.RawSignature(timestampedCosignature) | ||
| } | ||
|
|
||
| // serveCheckpoint writes latest's note, carrying the MTCA signature and a | ||
| // mirror cosignature, to the checkpoint path in tile storage. Per CQRP: "In | ||
| // order for landmarks to be served by Chrome's Landmark Service, all | ||
| // checkpoints MUST be served with a minimum of 2 cosignatures. One of these | ||
| // MUST be from the MTC CA Operator and one MUST be from a Mirroring Cosigner | ||
| // recognized by Chrome and not operated by the MTC CA Operator." | ||
| func (m *mtca) serveCheckpoint(ctx context.Context, latest *checkpointRow) error { | ||
| if latest.ID == m.servedCheckpointID { | ||
| return nil | ||
| } | ||
| if len(latest.RootHash) != tlog.HashSize { | ||
| return fmt.Errorf("checkpoint %d root hash is %d bytes, want %d", latest.ID, len(latest.RootHash), tlog.HashSize) | ||
| } | ||
|
|
||
| // Verify the MTCA cosignature and produce its signature line. | ||
| tree := tlog.Tree{N: latest.TreeSize, Hash: tlog.Hash(latest.RootHash)} | ||
| caCosignatureLine, err := m.verifier.SignatureLine(m.logID.Origin(), tree, latest.MTCASignature) | ||
| if err != nil { | ||
| return fmt.Errorf("checkpoint %d MTCA signature: %s", latest.ID, err) | ||
| } | ||
|
|
||
| // Verify the mirror cosignature and produce its signature line. | ||
| if latest.MirrorID != m.mirrorID { | ||
| return fmt.Errorf("checkpoint %d cosigned by mirror %q, want %q", latest.ID, latest.MirrorID, m.mirrorID) | ||
| } | ||
| mirrorCosignatureLine, err := m.mirrorVerifier.SignatureLine(m.logID.Origin(), tree, latest.MirrorSignature) | ||
| if err != nil { | ||
| return fmt.Errorf("checkpoint %d mirror cosignature: %s", latest.ID, err) | ||
| } | ||
|
|
||
| // Produce a note containing both cosgnatures. | ||
| cp := checkpoint.Checkpoint{Origin: m.logID.Origin(), Tree: tree} | ||
| note, err := cp.SignedNoteForServing(caCosignatureLine, mirrorCosignatureLine) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Finally, write it to the checkpoint path for serving. | ||
| err = tiles.WriteCheckpoint(ctx, m.s3c, m.logID.TilePrefix(), note) | ||
| if err != nil { | ||
| return fmt.Errorf("serving checkpoint %d: %s", latest.ID, err) | ||
| } | ||
| m.servedCheckpointID = latest.ID | ||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slight preference for a shared trees/config/ package which can hold both this and config.ID.