-
-
Notifications
You must be signed in to change notification settings - Fork 639
mtpublisher: Add skeleton that pushes dummy cosignatures #8793
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 |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package notmain | ||
|
|
||
| import ( | ||
| "context" | ||
| "flag" | ||
| "os" | ||
|
|
||
| "github.com/jmhodges/clock" | ||
|
|
||
| "github.com/letsencrypt/boulder/cmd" | ||
| "github.com/letsencrypt/boulder/config" | ||
| "github.com/letsencrypt/boulder/mtpublisher" | ||
| "github.com/letsencrypt/boulder/sa" | ||
| ) | ||
|
|
||
| type Config struct { | ||
| MTPublisher struct { | ||
| DB cmd.DBConfig | ||
|
|
||
| DebugAddr string `validate:"omitempty,hostname_port"` | ||
|
|
||
| // PollInterval is how often the stub scans for checkpoints that still | ||
| // lack a mirror cosignature. | ||
| PollInterval config.Duration `validate:"required"` | ||
|
|
||
| // MTCLogID is the log this MTPublisher operates on (e.g. | ||
| // "44947.4.1.0.44"). Used as a guard on the `mtcLogID` column of the | ||
| // connected checkpoints table. | ||
| MTCLogID string `validate:"required"` | ||
|
|
||
| // MirrorID identifies the cosigner this publisher writes alongside each | ||
| // cosignature (e.g. "32473.9"). | ||
| MirrorID string `validate:"required"` | ||
| } | ||
| Syslog cmd.SyslogConfig | ||
| OpenTelemetry cmd.OpenTelemetryConfig | ||
| } | ||
|
|
||
| func main() { | ||
| debugAddr := flag.String("debug-addr", "", "Debug server address override") | ||
| configFile := flag.String("config", "", "File path to the configuration file for this service") | ||
| flag.Parse() | ||
| if *configFile == "" { | ||
| flag.Usage() | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| var c Config | ||
| err := cmd.ReadConfigFile(*configFile, &c) | ||
| cmd.FailOnError(err, "Reading JSON config file into config structure") | ||
|
|
||
| if *debugAddr != "" { | ||
| c.MTPublisher.DebugAddr = *debugAddr | ||
| } | ||
|
|
||
| scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.MTPublisher.DebugAddr) | ||
| defer oTelShutdown(context.Background()) | ||
| cmd.LogStartup(logger) | ||
| clk := clock.New() | ||
|
|
||
| dbMap, err := sa.InitWrappedDb(c.MTPublisher.DB, scope, logger) | ||
| cmd.FailOnError(err, "While initializing dbMap") | ||
|
|
||
| publisher, err := mtpublisher.New(dbMap, c.MTPublisher.PollInterval.Duration, c.MTPublisher.MTCLogID, c.MTPublisher.MirrorID, clk, logger) | ||
| cmd.FailOnError(err, "Failed to create MTPublisher stub") | ||
|
|
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| go cmd.CatchSignals(cancel) | ||
| publisher.Start(ctx) | ||
| } | ||
|
|
||
| func init() { | ||
| cmd.RegisterCommand("boulder-mtpublisher", main, &cmd.ConfigValidator{Config: &Config{}}) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package mtpublisher | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/ed25519" | ||
| "database/sql" | ||
| "encoding/binary" | ||
| "errors" | ||
| "fmt" | ||
| "time" | ||
|
|
||
| "github.com/jmhodges/clock" | ||
|
|
||
| "github.com/letsencrypt/boulder/db" | ||
| blog "github.com/letsencrypt/boulder/log" | ||
| ) | ||
|
|
||
| // MTPublisher polls the MTC issuance log and adds a dummy cosignature to the | ||
| // latest checkpoint if it lacks one. It is a stub for the real MTPublisher. | ||
| type MTPublisher struct { | ||
| db *db.WrappedMap | ||
| interval time.Duration | ||
| mtcLogID string | ||
| mirrorID string | ||
| clk clock.Clock | ||
| log blog.Logger | ||
| } | ||
|
|
||
| // New returns a new *MTPublisher. | ||
| func New(dbMap *db.WrappedMap, interval time.Duration, mtcLogID, mirrorID string, clk clock.Clock, log blog.Logger) (*MTPublisher, error) { | ||
| if interval <= 0 { | ||
| return nil, fmt.Errorf("interval must be positive, got %s", interval) | ||
| } | ||
| if mtcLogID == "" { | ||
| return nil, errors.New("mtcLogID must not be empty") | ||
| } | ||
| if mirrorID == "" { | ||
| return nil, errors.New("mirrorID must not be empty") | ||
| } | ||
| return &MTPublisher{ | ||
| db: dbMap, | ||
| interval: interval, | ||
| mtcLogID: mtcLogID, | ||
| mirrorID: mirrorID, | ||
| clk: clk, | ||
| log: log, | ||
| }, nil | ||
| } | ||
|
|
||
| type checkpointEntry struct { | ||
| ID int64 `db:"id"` | ||
| MTCLogID string `db:"mtcLogID"` | ||
| TreeSize int64 `db:"treeSize"` | ||
| MirrorSignature []byte `db:"mirrorSignature"` | ||
| } | ||
|
|
||
| // dummyCosignature returns a dummy Ed25519 tlog-cosignature: a big-endian | ||
| // uint64 timestamp followed by the Ed25519 signature. | ||
|
Comment on lines
+57
to
+58
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. Two comments:
Member
Author
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.
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. FWIW, when I added genmtpki.go I made the wrong choice (to use ECDSA), not realizing that tlog only specifies Ed25519 and ML-DSA-44. I can update that - either to Ed25519, or possibly straight to ML-DSA once #8787 is resolved. |
||
| func (p *MTPublisher) dummyCosignature() []byte { | ||
| out := make([]byte, 8+ed25519.SignatureSize) | ||
| binary.BigEndian.PutUint64(out[:8], uint64(p.clk.Now().Unix())) //nolint:gosec // G115: a Unix timestamp is non-negative. | ||
| return out | ||
| } | ||
|
|
||
| func (p *MTPublisher) publish(ctx context.Context) error { | ||
| var latest checkpointEntry | ||
| err := p.db.SelectOne(ctx, &latest, | ||
| "SELECT id, mtcLogID, treeSize, mirrorSignature FROM checkpoints WHERE mtcLogID = ? ORDER BY treeSize DESC LIMIT 1", | ||
| p.mtcLogID) | ||
| if errors.Is(err, sql.ErrNoRows) { | ||
| return nil | ||
| } | ||
| if err != nil { | ||
| return fmt.Errorf("selecting the latest checkpoint: %w", err) | ||
| } | ||
| if latest.MirrorSignature != nil { | ||
| return nil | ||
| } | ||
|
|
||
| _, err = p.db.ExecContext(ctx, | ||
| "UPDATE checkpoints SET mirrorID = ?, mirrorSignature = ? WHERE id = ? AND mtcLogID = ?", | ||
| p.mirrorID, p.dummyCosignature(), latest.ID, p.mtcLogID) | ||
| if err != nil { | ||
| return fmt.Errorf("cosigning checkpoint %d (%s size %d): %w", latest.ID, latest.MTCLogID, latest.TreeSize, err) | ||
| } | ||
| p.log.Infof("Cosigned checkpoint %d (%s size %d)", latest.ID, latest.MTCLogID, latest.TreeSize) | ||
| return nil | ||
| } | ||
|
|
||
| // Start attempts to cosign the latest checkpoint at each interval until ctx is | ||
| // cancelled. | ||
| func (p *MTPublisher) Start(ctx context.Context) { | ||
| ticker := time.NewTicker(p.interval) | ||
| defer ticker.Stop() | ||
| for { | ||
| err := p.publish(ctx) | ||
| if err != nil { | ||
| p.log.Errf("Cosigning pass failed: %s", err) | ||
| } | ||
| select { | ||
| case <-ctx.Done(): | ||
| return | ||
| case <-ticker.C: | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| package mtpublisher | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/ed25519" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/jmhodges/clock" | ||
|
|
||
| "github.com/letsencrypt/boulder/db" | ||
| blog "github.com/letsencrypt/boulder/log" | ||
| "github.com/letsencrypt/boulder/sa" | ||
| "github.com/letsencrypt/boulder/test/vars" | ||
| ) | ||
|
|
||
| const ( | ||
| mtcLogID = "44947.4.1.0.44" | ||
| mirrorID = "32473.9" | ||
| ) | ||
|
|
||
| func setupDB(t *testing.T) *db.WrappedMap { | ||
| t.Helper() | ||
|
|
||
| dbMap, err := sa.DBMapForTest(vars.DBConnMTCMeta_44947_4_1_0_44FullPerms) | ||
| if err != nil { | ||
| t.Fatalf("opening mtcmeta dbMap: %s", err) | ||
| } | ||
| _, err = dbMap.ExecContext(t.Context(), "TRUNCATE TABLE checkpoints") | ||
| if err != nil { | ||
| t.Fatalf("truncating checkpoints: %s", err) | ||
| } | ||
| t.Cleanup(func() { | ||
| _, err := dbMap.ExecContext(context.Background(), "TRUNCATE TABLE checkpoints") | ||
| if err != nil { | ||
| t.Logf("cleaning up checkpoints: %s", err) | ||
| } | ||
| }) | ||
| return dbMap | ||
| } | ||
|
|
||
| func insertCheckpoint(t *testing.T, dbMap *db.WrappedMap, logID string, treeSize int64) int64 { | ||
| t.Helper() | ||
|
|
||
| res, err := dbMap.ExecContext(t.Context(), | ||
| "INSERT INTO checkpoints (mtcLogID, mtcaSignature, treeSize, rootHash) VALUES (?, ?, ?, ?)", | ||
| logID, []byte("mtca-signature"), treeSize, make([]byte, 32)) | ||
| if err != nil { | ||
| t.Fatalf("inserting checkpoint (%s size %d): %s", logID, treeSize, err) | ||
| } | ||
| id, err := res.LastInsertId() | ||
| if err != nil { | ||
| t.Fatalf("reading insert id: %s", err) | ||
| } | ||
| return id | ||
| } | ||
|
|
||
| func lacksCosignature(t *testing.T, dbMap *db.WrappedMap, id int64) bool { | ||
| t.Helper() | ||
| var count int64 | ||
| err := dbMap.SelectOne(t.Context(), &count, | ||
| "SELECT COUNT(*) FROM checkpoints WHERE id = ? AND mirrorID IS NULL AND mirrorSignature IS NULL", id) | ||
| if err != nil { | ||
| t.Fatalf("querying checkpoint %d: %s", id, err) | ||
| } | ||
| return count == 1 | ||
| } | ||
|
|
||
| func TestPublish(t *testing.T) { | ||
| dbMap := setupDB(t) | ||
| p, err := New(dbMap, time.Second, mtcLogID, mirrorID, clock.NewFake(), blog.NewMock()) | ||
| if err != nil { | ||
| t.Fatalf("New: %s", err) | ||
| } | ||
|
|
||
| // When there are no checkpoints at all, p.publish() should return without | ||
| // error. | ||
| err = p.publish(t.Context()) | ||
| if err != nil { | ||
| t.Fatalf("p.publish() on an empty table: %s", err) | ||
| } | ||
|
|
||
| // An older checkpoint that is not cosigned, which must be left untouched. | ||
| olderCheckpointID := insertCheckpoint(t, dbMap, mtcLogID, 256) | ||
|
|
||
| // The latest checkpoint, which we expect to be cosigned by p.publish(). | ||
| latestCheckpointID := insertCheckpoint(t, dbMap, mtcLogID, 512) | ||
|
|
||
| // A checkpoint for another log that was somehow inserted into this table, | ||
| // which must be left untouched thanks to the mtcLogID guard. | ||
| otherLogID := insertCheckpoint(t, dbMap, "44947.4.2.0.99", 1024) | ||
|
|
||
| err = p.publish(t.Context()) | ||
| if err != nil { | ||
| t.Fatalf("p.publish(): %s", err) | ||
| } | ||
|
|
||
| // Fetch the latest checkpoint. | ||
| type row struct { | ||
| MirrorID string `db:"mirrorID"` | ||
| MirrorSig []byte `db:"mirrorSignature"` | ||
| } | ||
| var cosigned row | ||
| err = dbMap.SelectOne(t.Context(), &cosigned, "SELECT mirrorID, mirrorSignature FROM checkpoints WHERE id = ?", latestCheckpointID) | ||
| if err != nil { | ||
| t.Fatalf("selecting the latest checkpoint: %s", err) | ||
| } | ||
|
|
||
| // Check that the latest checkpoint was cosigned, and the others were | ||
| // untouched. | ||
| if cosigned.MirrorID != mirrorID { | ||
| t.Errorf("mirrorID = %q, want %q", cosigned.MirrorID, mirrorID) | ||
| } | ||
| if len(cosigned.MirrorSig) != 8+ed25519.SignatureSize { | ||
| t.Errorf("latest checkpoint's mirrorSignature is %d bytes, want %d", len(cosigned.MirrorSig), 8+ed25519.SignatureSize) | ||
| } | ||
| if !lacksCosignature(t, dbMap, olderCheckpointID) { | ||
| t.Error("older checkpoint was cosigned, only the latest should be") | ||
| } | ||
| if !lacksCosignature(t, dbMap, otherLogID) { | ||
| t.Errorf("otherLogID checkpoint (id=%d), despite guard on mtcLogID", otherLogID) | ||
| } | ||
| } | ||
|
|
||
| func TestPublishWhenLatestAlreadySigned(t *testing.T) { | ||
| dbMap := setupDB(t) | ||
| p, err := New(dbMap, time.Second, mtcLogID, mirrorID, clock.NewFake(), blog.NewMock()) | ||
| if err != nil { | ||
| t.Fatalf("New: %s", err) | ||
| } | ||
|
|
||
| // Insert a checkpoint that is already cosigned, which must be left | ||
| // untouched. | ||
| _, err = dbMap.ExecContext(t.Context(), | ||
| "INSERT INTO checkpoints (mtcLogID, mtcaSignature, treeSize, rootHash, mirrorID, mirrorSignature) VALUES (?, ?, ?, ?, ?, ?)", | ||
| mtcLogID, []byte("mtca-signature"), int64(512), make([]byte, 32), "existing.cosigner", []byte("already-signed-bruh")) | ||
| if err != nil { | ||
| t.Fatalf("inserting cosigned checkpoint: %s", err) | ||
| } | ||
|
|
||
| // Insert an older (non-latest) checkpoint that is not cosigned, which must | ||
| // be left untouched. | ||
| olderID := insertCheckpoint(t, dbMap, mtcLogID, 256) | ||
|
|
||
| err = p.publish(t.Context()) | ||
| if err != nil { | ||
| t.Fatalf("p.publish(): %s", err) | ||
| } | ||
|
|
||
| // The latest checkpoint is already cosigned and the older checkpoint is left untouched. | ||
| if !lacksCosignature(t, dbMap, olderID) { | ||
| t.Error("older checkpoint was cosigned, the pass should have stopped at the signed latest") | ||
| } | ||
| var cosignature []byte | ||
| err = dbMap.SelectOne(t.Context(), &cosignature, "SELECT mirrorSignature FROM checkpoints WHERE mtcLogID = ? AND treeSize = 512", mtcLogID) | ||
| if err != nil { | ||
| t.Fatalf("selecting the cosigned checkpoint: %s", err) | ||
| } | ||
| if string(cosignature) != "already-signed-bruh" { | ||
| t.Errorf("existing cosignature was replaced: %q", cosignature) | ||
| } | ||
| } |
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.
hyper-nit: indicate that this is a placeholder value. totally optional if this will be replaced quickly.