Skip to content

mtpublisher: Replace the stub with a tlog-mirror client - #8973

Open
beautifulentropy wants to merge 3 commits into
paving-a-path-to-a-proper-publisher-06afrom
paving-a-path-to-a-proper-publisher-08
Open

mtpublisher: Replace the stub with a tlog-mirror client#8973
beautifulentropy wants to merge 3 commits into
paving-a-path-to-a-proper-publisher-06afrom
paving-a-path-to-a-proper-publisher-08

Conversation

@beautifulentropy

@beautifulentropy beautifulentropy commented Aug 24, 2026

Copy link
Copy Markdown
Member
  • mtpublisher: Add MirrorClient, submitting the checkpoint to add-checkpoint, uploading entries to add-entries, and obtaining the cosignature from sign-subtree
  • mtpublisher: Add Source, building consistency proofs and entry packages from tile storage
  • mtpublisher: Reconstruct the MTCA signature line from the stored signature and verify the signed note before submission
  • mtpublisher: Configure the MTCA certificate and the mirror's ID, public key, and base URL, replacing the mirror signing key
  • mtpublisher/mtpublishertest: Add TestMirror, for cosigning in unit tests
  • test: Start bsunlight with the boulder container and make its entrypoint executable
  • test/sunlight/genkeys: Stop writing the mirror's private key

Cleanup:

  • trees/checkpoint: Remove SignedNoteForMirror and SignedNoteForSignSubtree, replaced by SignedNote
  • trees/cosignature: Remove TimestampedSignature, replaced by FilterByVerify

Closes #8944

@beautifulentropy
beautifulentropy marked this pull request as ready for review August 24, 2026 16:18
@beautifulentropy
beautifulentropy requested a review from a team as a code owner August 24, 2026 16:18
@beautifulentropy
beautifulentropy requested a review from jsha August 24, 2026 16:18
@github-actions

Copy link
Copy Markdown
Contributor

@beautifulentropy, this PR appears to contain configuration and/or SQL schema changes. Please ensure that a corresponding deployment ticket has been filed with the new values.

Comment thread cmd/boulder-mtpublisher/main.go Outdated
Comment thread cmd/boulder-mtpublisher/main.go Outdated
Comment thread mtpublisher/mirror.go
Comment thread mtpublisher/mirror.go Outdated
Comment thread mtpublisher/mirror.go Outdated
Comment thread mtpublisher/mirror.go Outdated
Comment thread mtpublisher/mirror.go
Comment thread mtpublisher/source.go Outdated
Comment thread mtpublisher/mtpublisher.go Outdated
@beautifulentropy
beautifulentropy force-pushed the paving-a-path-to-a-proper-publisher-08 branch from e55287c to bffa98f Compare August 27, 2026 18:37
Comment thread cmd/boulder-mtpublisher/main.go Outdated
Comment thread cmd/config.go
Comment thread mtpublisher/mirror.go Outdated
Comment thread mtpublisher/mtpublisher.go Outdated
@beautifulentropy
beautifulentropy force-pushed the paving-a-path-to-a-proper-publisher-08 branch from bffa98f to bf00239 Compare September 1, 2026 21:44
@beautifulentropy
beautifulentropy changed the base branch from paving-a-path-to-a-proper-publisher-07 to paving-a-path-to-a-proper-publisher-06a September 1, 2026 22:37
@beautifulentropy
beautifulentropy force-pushed the paving-a-path-to-a-proper-publisher-08 branch from bf00239 to 4eb420e Compare September 1, 2026 22:39
@beautifulentropy
beautifulentropy force-pushed the paving-a-path-to-a-proper-publisher-08 branch from 4eb420e to 8ecc8ba Compare September 2, 2026 16:52
@beautifulentropy
beautifulentropy force-pushed the paving-a-path-to-a-proper-publisher-08 branch from 8ecc8ba to 6158fbc Compare September 2, 2026 16:57
Comment thread cmd/config.go
Comment on lines +112 to +113
pubKey, err := loadMLDSAPublicKey(c.MTPublisher.Mirror.PublicKeyFile)
cmd.FailOnError(err, "Loading mirror public key")

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.

nit: move this down to line 122. This pubkey isn't used until we construct the mirror, so all the MTCA-loading code shouldn't be between those two operations.

if err != nil {
t.Fatalf("Marshal: %s", err)
}
_, err = verifier.FilterByVerify(text, line)

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.

You're throwing away the signature returned by FilterByVerify, so why not just use verifier.Verify here?

Comment thread mtpublisher/mirror.go
return fmt.Errorf("add-checkpoint at tree size %d got 409 with mirror tree size %d after retrying", tree.N, mirrorSize)
}
retried = true
oldSize = mirrorSize

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.

Why save this to a local variable and then only write it to m.oldSize after the retry succeeds? The mirror has affirmatively told us that its current size is mirrorSize, I don't see any reason to trust our own understanding of the mirror's size (from a successful write) more than the mirror's own understanding of its current size (from the conflict response).

Is there an error case I'm misunderstanding?

Comment thread mtpublisher/mirror.go
if err != nil {
return nil, fmt.Errorf("marshaling the checkpoint: %w", err)
}
timestampedMirrorCosignature, err := m.verifier.FilterByVerify(noteText, subtreeCosignatureLines)

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.

Feels weird to call this a timestamped mirror cosignature, since it has to be the zero timestamp.

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.

FWIW, I think this naming makes sense because it's referring to the data format (timestamped_signature from https://github.com/C2SP/C2SP/blob/main/tlog-cosignature.md#format) and contrasts with the rawMirrorSignature below.

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.

Yep, I thought about that. The name makes sense, but IMO is surprising enough that it can cause a reader (as it caused me!) to stop and question what they think they know about subtree cosignatures. I think a name like zeroTimestampMirrorCosignature would be clearer, but we're very much into bikeshed territory. I'm happy with whatever path Samantha picks.

return fmt.Errorf("assembling checkpoint %d signed note: %w", latest.ID, err)
}
timestampedMirrorCosig, err := cosignature.TimestampedSignature(text, []byte(cosigLine), p.verifier)
_, _, err = checkpoint.Open(signedNoteForMirror, p.caVerifier)

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.

Here we're using Open just for its validation capabilities, because we're throwing away the results of parsing the note.

That means that we have four ways to validate a checkpoint:

  • checkpoint.Open(signedNote, verifier)
  • verifier.FilterByVerify(noteText, signatureLines)
  • verifier.Verify(noteText, timestampedSignature)
  • verifier.VerifyCheckpoint(origin, tree, timestampedSignature)

(I'm ignoring the top-level cosignature.SignatureLine, which also does verification, because it is removed in the next PR in this stack.)

I'm of the opinion that this is too much. I'm not exactly sure what the best solution is -- obviously each of these methods is convenient in certain places, and less convenient in others -- but it suggests to me that we don't have a clear sense of what a checkpoint is. This feeling seems corroborated to me by the fact that we don't even have a struct type representing a whole Checkpoint, including signatures: we only have a struct for the Checkpoint note text, and signatures are only ever attached to their note in the form of serialized text.

I think the underlying issue is that everything else is optimized to only handle true checkpoint objects when absolutely necessary: the MTCA doesn't produce checkpoints, it just produces raw signatures and tosses those in the database to be turned into checkpoints later; the tlog-mirror add-entries endpoint doesn't return a checkpoint when you're done uploading, it just returns signature lines and trusts you to construct the rest of the checkpoint object.

We could instead take the principled stand that we only ever pass around checkpoint objects. The MTCA's signing routine produces a signature, immediately bundles it into a Checkpoint object with signature line, and returns that. We use a new checkpoint.RawSignature() accessor method to extract the sig bytes and write them to the database, and we write checkpoint.Text() to S3. Similarly, treedb.LatestCheckpoint() would return a Checkpoint object so that the publisher doesn't have to do all this assembly work here, and mirror.Cosign would return a Checkpoint object that can have the signature extracted for the database just like the MTCA's.

Obviously this design sketch goes beyond what should be part of this stack of PRs. But maybe you have other ideas about how to prevent the proliferation of similar-but-not-identical verification functions, and maybe some of your ideas are better and less invasive than mine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mtpublisher: Publish checkpoint entries read from tiles

3 participants