Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions cmd/boulder-mtca/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ package notmain

import (
"context"
"crypto/mldsa"
"crypto/x509"
"database/sql"
"encoding/pem"
"errors"
"flag"
"fmt"
"os"
"sync"
"time"
Expand Down Expand Up @@ -50,12 +54,37 @@ type Config struct {

// SequencingPeriod controls how frequently the MTCA sequences a batch and signs a checkpoint.
SequencingPeriod config.Duration `validate:"required"`

// Mirror identifies the mirror whose cosignatures must be verified
// before the MTCA serves them as part of a checkpoint.
Mirror cmd.MirrorConfig `validate:"required"`

Copy link
Copy Markdown
Contributor

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.

}

Syslog cmd.SyslogConfig
OpenTelemetry cmd.OpenTelemetryConfig
}

// loadMLDSAPublicKey reads a PEM-encoded PKIX ML-DSA-44 public key.
func loadMLDSAPublicKey(filename string) (*mldsa.PublicKey, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
block, _ := pem.Decode(data)
if block == nil || block.Type != "PUBLIC KEY" {
return nil, fmt.Errorf("no PUBLIC KEY PEM block in %s", filename)
}
parsed, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
pubKey, ok := parsed.(*mldsa.PublicKey)
if !ok {
return nil, fmt.Errorf("key in %s is %T, must be ML-DSA-44", filename, parsed)
}
return pubKey, nil
}

func main() {
grpcAddr := flag.String("addr", "", "gRPC listen address override")
debugAddr := flag.String("debug-addr", "", "Debug server address override")
Expand Down Expand Up @@ -117,13 +146,18 @@ func main() {
s3c, err := bs3.FromConfig(c.MTCA.S3, logger)
cmd.FailOnError(err, "Loading S3 config")

mirrorPublicKey, err := loadMLDSAPublicKey(c.MTCA.Mirror.PublicKeyFile)
cmd.FailOnError(err, "Loading mirror public key")

mtcaImpl, err := mtca.New(
issuer,
profiles,
c.MTCA.LogID,
c.MTCA.SequencingPeriod.Duration,
dbMap,
s3c,
c.MTCA.Mirror.ID,
mirrorPublicKey,
logger,
clk)
cmd.FailOnError(err, "Building MTCA")
Expand Down
88 changes: 77 additions & 11 deletions mtca/mtca.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand All @@ -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,
}
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
}

Expand All @@ -104,6 +115,11 @@ type mtca struct {
cosigner *cosignature.Cosigner
verifier *cosignature.Verifier

mirrorID string
mirrorVerifier *cosignature.Verifier

servedCheckpointID int64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, .New() should read from S3 to populate this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally Preflight() rather than New(); that's where we do other storage-involved work at startup. It's nice to be able to separate that from simply constructing the object.


pool *pool

// frontier contains all the tiles on the right edge of the tree.
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
13 changes: 13 additions & 0 deletions mtca/mtca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,19 @@ func setup() (*mtca, *bs3test.FakeS3, func(), error) {
}

fs3 := bs3test.New()
mirrorKey, err := mldsa.NewPrivateKey(mldsa.MLDSA44(), make([]byte, 32))
if err != nil {
return nil, nil, nil, err
}
mtca, err := New(
issuer,
map[string]*issuance.Profile{"mtcExample": profile},
issuancelog.ID{CAID: "44947.4.1", LogNumber: 44},
100*time.Millisecond,
dbMap,
fs3,
"32473.9",
mirrorKey.PublicKey(),
logger,
clk)
if err != nil {
Expand Down Expand Up @@ -479,6 +485,13 @@ func TestSequenceStorageFailure(t *testing.T) {
t.Cleanup(cleanup)
mirrorCosign(t, mtca)

// Serve the cosigned checkpoint while storage is healthy, so the failure
// below lands on staging.
err = mtca.sequence(t.Context())
if err != nil {
t.Fatalf("sequencing to serve the cosigned checkpoint: %s", err)
}

mtca.pool.maxSize = 2
results := issueMany(t, mtca, 2)

Expand Down
4 changes: 4 additions & 0 deletions test/config-next/mtca.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"logNumber": 44
},
"sequencingPeriod": "100ms",
"mirror": {
"id": "32473.9",
"publicKeyFile": "test/certs/mtpki/mirror.pub.pem"
},
"db": {
"dbConnectFile": "test/secrets/mtca1_dburl"
},
Expand Down
Loading