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
7 changes: 6 additions & 1 deletion cmd/boulder-mtpublisher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ type Config struct {
// endpoints (e.g. "http://localhost:4700").
BaseURL string `validate:"required,url"`

// MonitoringPrefix is the mirror's tlog-mirror monitoring prefix,
// under which it serves its copy of the log and its cosigned
// checkpoint (e.g. "http://localhost:8333/mirror").
MonitoringPrefix string `validate:"required,url"`

// Timeout bounds each request to the mirror.
Timeout config.Duration `validate:"required"`
}
Expand Down Expand Up @@ -124,7 +129,7 @@ func main() {
mirrorPubKey, err := loadMLDSAPublicKey(c.MTPublisher.Mirror.PublicKeyFile)
cmd.FailOnError(err, "Loading mirror public key")

mirror, err := mtpublisher.NewMirrorClient(c.MTPublisher.Mirror.BaseURL, mtpublisher.NewSource(s3c, c.MTPublisher.LogID.TilePrefix()), c.MTPublisher.Mirror.ID, mirrorPubKey, c.MTPublisher.Mirror.Timeout.Duration)
mirror, err := mtpublisher.NewMirrorClient(c.MTPublisher.Mirror.BaseURL, c.MTPublisher.Mirror.MonitoringPrefix, mtpublisher.NewSource(s3c, c.MTPublisher.LogID.TilePrefix()), c.MTPublisher.Mirror.ID, mirrorPubKey, c.MTPublisher.Mirror.Timeout.Duration)
cmd.FailOnError(err, "Creating mirror client")

publisher, err := mtpublisher.New(treedb.New(dbMap), c.MTPublisher.PollInterval.Duration, c.MTPublisher.LogID, caPubKey, mirror, logger)
Expand Down
29 changes: 2 additions & 27 deletions mtca/mtca.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
mtcapb "github.com/letsencrypt/boulder/mtca/proto"
"github.com/letsencrypt/boulder/trees/checkpoint"
"github.com/letsencrypt/boulder/trees/cosignature"
"github.com/letsencrypt/boulder/trees/cosigned"
"github.com/letsencrypt/boulder/trees/entry"
"github.com/letsencrypt/boulder/trees/issuancelog"
"github.com/letsencrypt/boulder/trees/pubkey"
Expand Down Expand Up @@ -576,33 +575,9 @@ func (m *mtca) sequence(ctx context.Context) error {
return fmt.Errorf("publishing tiles: %s", err)
}

// `cosigner_name` and `log_origin` are computed from the cosigner ID and
// the issuance log's ID (Section 5.1), respectively. They contain the
// concatenation of:
// * The 16-byte ASCII string `oid/1.3.6.1.4.1.`
// * The trust anchor ID's ASCII representation (Section 4 of [I-D.ietf-tls-trust-anchor-ids])
// This is equivalent to the concatenation of:
// * The four-byte ASCII string `oid/`
// * The trust anchor ID as a full OID, in dotted decimal notation
//
// https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#name-signature-format
cosignedMessage, err := (&cosigned.Message{
CosignerName: "oid/1.3.6.1.4.1." + m.logID.CAID,
// Timestamp should always be zero when signing over a subtree (as
// opposed to a checkpoint)
Timestamp: 0,
LogOrigin: "oid/1.3.6.1.4.1." + m.logID.String(),
Start: 0,
End: uint64(m.frontier.TreeSize()), //nolint:gosec // G115: guaranteed non-negative by Frontier
SubtreeHash: newRootHash,
}).Marshal()
subtreeSignature, err := m.cosigner.CosignSubtree(0, m.frontier.TreeSize(), newRootHash)
if err != nil {
return fmt.Errorf("marshaling cosigned message: %s", err)
}

subtreeSignature, err := m.issuer.Signer.Sign(nil, cosignedMessage, nil)
if err != nil {
return err
return fmt.Errorf("signing subtree: %s", err)
}

// TODO(#9020)(#9038): calculate specific subtree coverage rather than
Expand Down
34 changes: 32 additions & 2 deletions mtca/mtca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,33 @@ func (f *fakeDB) InsertCheckpointSubtree(ctx context.Context, s *treedb.Checkpoi
return s.ID, nil
}

func (f *fakeDB) GetUnsignedSubtrees(ctx context.Context, mtcLogID string) ([]*treedb.CheckpointSubtreeModel, error) {
if mtcLogID != f.mtcLogID {
return nil, fmt.Errorf("wrong log")
}
var subtrees []*treedb.CheckpointSubtreeModel
for i := range f.checkpointSubtrees {
if f.checkpointSubtrees[i].MirrorID == nil {
subtrees = append(subtrees, &f.checkpointSubtrees[i])
}
}
return subtrees, nil
}

func (f *fakeDB) AddSubtreeMirrorSignature(ctx context.Context, id int64, mirrorID string, mirrorSignature []byte, mtcLogID string) error {
if mtcLogID != f.mtcLogID {
return fmt.Errorf("wrong log")
}
for i, s := range f.checkpointSubtrees {
if s.ID == id {
f.checkpointSubtrees[i].MirrorID = &mirrorID
f.checkpointSubtrees[i].MirrorSignature = mirrorSignature
return nil
}
}
return fmt.Errorf("subtree %d not found", id)
}

func (f *fakeDB) getCheckpointSubtree(mtcLogID string, id int64) (*treedb.CheckpointSubtreeModel, error) {
if mtcLogID != f.mtcLogID {
return nil, fmt.Errorf("wrong log")
Expand Down Expand Up @@ -614,9 +641,9 @@ func mirrorCosign(t *testing.T, m *mtca) {
if err != nil {
t.Fatalf("mtpublisher.New: %s", err)
}
err = p.Publish(t.Context())
err = p.PublishCheckpoint(t.Context())
if err != nil {
t.Fatalf("mtpublisher.Publish: %s", err)
t.Fatalf("mtpublisher.PublishCheckpoint: %s", err)
}
}

Expand Down Expand Up @@ -813,6 +840,9 @@ func TestSequence(t *testing.T) {
if len(subtree.MTCASignature) == 0 {
t.Error("subtree.MTCASignature: got empty slice, want non-empty slice")
}
if !mtca.verifier.VerifySubtree(mtca.logID.Origin(), int64(subtree.SubtreeStart), int64(subtree.SubtreeEnd), tlog.Hash(subtree.SubtreeHash), subtree.MTCASignature) { //nolint:gosec // G115: the subtree covers the 6 entry tree
t.Error("subtree.MTCASignature: does not verify")
}

if subtree.MirrorID != nil {
t.Error("subtree.MirrorID: got non-nil string pointer, want nil")
Expand Down
137 changes: 112 additions & 25 deletions mtpublisher/mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"compress/gzip"
"context"
"crypto/mldsa"
"crypto/sha256"
"errors"
"fmt"
"io"
Expand All @@ -18,6 +19,7 @@ import (
"github.com/letsencrypt/boulder/trees/checkpoint"
"github.com/letsencrypt/boulder/trees/cosignature"
"github.com/letsencrypt/boulder/trees/mirror"
"github.com/letsencrypt/boulder/trees/subtree"
)

// maxMirrorResponseSize caps how much of a mirror's response body the client
Expand Down Expand Up @@ -45,6 +47,7 @@ var _ Mirror = (*MirrorClient)(nil)
// entries to add-entries until the mirror cosigns.
type MirrorClient struct {
submissionPrefix string
monitoringPrefix string
client *http.Client
src *Source
mirrorID string
Expand All @@ -61,11 +64,15 @@ type MirrorClient struct {
}

// NewMirrorClient returns a MirrorClient that submits to the mirror's endpoints
// under baseURL, giving each request timeout to complete.
func NewMirrorClient(baseURL string, src *Source, mirrorID string, mirrorPublicKey *mldsa.PublicKey, timeout time.Duration) (*MirrorClient, error) {
// under baseURL and reads its checkpoint under monitoringPrefix, giving each
// request timeout to complete.
func NewMirrorClient(baseURL, monitoringPrefix string, src *Source, mirrorID string, mirrorPublicKey *mldsa.PublicKey, timeout time.Duration) (*MirrorClient, error) {
if baseURL == "" {
return nil, errors.New("empty mirror base URL")
}
if monitoringPrefix == "" {
return nil, errors.New("empty mirror monitoring prefix")
}
if timeout <= 0 {
return nil, fmt.Errorf("timeout must be positive, got %s", timeout)
}
Expand All @@ -75,6 +82,7 @@ func NewMirrorClient(baseURL string, src *Source, mirrorID string, mirrorPublicK
}
return &MirrorClient{
submissionPrefix: baseURL,
monitoringPrefix: monitoringPrefix,
client: &http.Client{Timeout: timeout},
src: src,
mirrorID: mirrorID,
Expand Down Expand Up @@ -216,7 +224,7 @@ func (m *MirrorClient) addEntries(ctx context.Context, origin string, tree tlog.
}
if info.TreeSize != tree.N {
// The spec has the client adopt the advertised tree size, but
// Cosign set the mirror's pending checkpoint to tree.N with
// CosignCheckpoint set the mirror's pending checkpoint to tree.N with
// add-checkpoint before uploading, so the mirror SHOULD have
// echoed it, and a cosignature over any other tree size is no
// use here.
Expand All @@ -238,11 +246,10 @@ func (m *MirrorClient) addEntries(ctx context.Context, origin string, tree tlog.
}
}

// signSubtree requests the mirror's zero timestamp signature over the whole
// tree from the c2sp.org/tlog-witness sign-subtree endpoint, presenting the
// checkpoint note carrying the cosignature lines add-entries returned.
func (m *MirrorClient) signSubtree(ctx context.Context, tree tlog.Tree, signedNote []byte) ([]byte, error) {
body, err := mirror.SignSubtreeRequest(0, tree.N, tree.Hash, nil, signedNote)
// signSubtree requests the mirror's zero timestamp signature over the subtree
// [start, end) from the c2sp.org/tlog-witness sign-subtree endpoint.
func (m *MirrorClient) signSubtree(ctx context.Context, start, end int64, subtreeHash tlog.Hash, proof []tlog.Hash, signedNote []byte) ([]byte, error) {
body, err := mirror.SignSubtreeRequest(start, end, subtreeHash, proof, signedNote)
if err != nil {
return nil, err
}
Expand All @@ -256,13 +263,13 @@ func (m *MirrorClient) signSubtree(ctx context.Context, tree tlog.Tree, signedNo
return respBody, nil
}

// Cosign runs the c2sp.org/tlog-mirror submission protocol for the checkpoint
// and returns the mirror's raw signature over the whole tree from sign-subtree,
// CosignCheckpoint runs the c2sp.org/tlog-mirror submission protocol for the
// checkpoint and returns the mirror's cosignature line from add-entries,
// verified against the mirror's key. A mirror that is already up to date,
// whether from an earlier submission whose cosignature was never stored or
// from another submitter, still gets the full exchange, since that is the only
// way to obtain its cosignature.
func (m *MirrorClient) Cosign(ctx context.Context, cp *checkpoint.Checkpoint, signedNoteForMirror []byte) ([]byte, error) {
func (m *MirrorClient) CosignCheckpoint(ctx context.Context, cp *checkpoint.Checkpoint, signedNoteForMirror []byte) ([]byte, error) {
// Submit the checkpoint to the mirror.
err := m.addCheckpoint(ctx, cp.Tree, signedNoteForMirror)
if err != nil {
Expand All @@ -275,30 +282,110 @@ func (m *MirrorClient) Cosign(ctx context.Context, cp *checkpoint.Checkpoint, si
return nil, fmt.Errorf("add-entries: %w", err)
}

// Exchange the mirror's cosignature for its subtree signature.
noteForSignSubtree, err := cp.SignedNote(mirrorCosignatureLines)
// Verify the mirror's signature.
noteText, err := cp.Marshal()
if err != nil {
return nil, fmt.Errorf("assembling the sign-subtree note: %w", err)
return nil, fmt.Errorf("marshaling the checkpoint: %w", err)
}
subtreeCosignatureLines, err := m.signSubtree(ctx, cp.Tree, noteForSignSubtree)
checkpointCosignature, err := m.verifier.FilterByVerify(noteText, mirrorCosignatureLines)
if err != nil {
return nil, fmt.Errorf("sign-subtree: %w", err)
return nil, fmt.Errorf("checkpoint cosignature failed verification: %w", err)
}
if checkpointCosignature.Timestamp == 0 {
return nil, errors.New("checkpoint cosignature has a zero timestamp")
}
return checkpointCosignature.Line, nil
}

// Verify the mirror's signature.
noteText, err := cp.Marshal()
// mirrorCheckpoint returns the mirror's checkpoint of the log with the given
// origin and its signed note, with the mirror's cosignature verified.
//
// https://c2sp.org/tlog-mirror#introduction
func (m *MirrorClient) mirrorCheckpoint(ctx context.Context, origin string) (*checkpoint.Checkpoint, []byte, error) {
endpoint, err := url.JoinPath(m.monitoringPrefix, fmt.Sprintf("%x", sha256.Sum256([]byte(origin))), "checkpoint")
if err != nil {
return nil, fmt.Errorf("marshaling the checkpoint: %w", err)
return nil, nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, nil, err
}
resp, err := m.client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxMirrorResponseSize))
if err != nil {
return nil, nil, fmt.Errorf("reading mirror checkpoint: %s", err)
}
if resp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("mirror returned status %d for its checkpoint: %s", resp.StatusCode, errorBody(body))
}

separator := bytes.Index(body, []byte("\n\n"))
if separator < 0 {
return nil, nil, errors.New("mirror checkpoint has no signature lines")
}
zeroTimestampMirrorCosignature, err := m.verifier.FilterByVerify(noteText, subtreeCosignatureLines)
noteText := body[:separator+1]
signatureLines := body[separator+2:]
Comment on lines +326 to +331

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.

I think we can replace this parsing code with checkpoint.Open(). If that's not possible we should have a comment explaining.

mirrored, err := checkpoint.Unmarshal(noteText)
if err != nil {
return nil, fmt.Errorf("cosignature failed verification: %w", err)
return nil, nil, fmt.Errorf("parsing mirror checkpoint: %w", err)
}
if mirrored.Origin != origin {
return nil, nil, fmt.Errorf("mirror checkpoint is of %s, want %s", mirrored.Origin, origin)
}
cosig, err := m.verifier.FilterByVerify(noteText, signatureLines)
if err != nil {
return nil, nil, fmt.Errorf("mirror checkpoint cosignature failed verification: %w", err)
}
signedNote, err := mirrored.SignedNote(cosig.Line)
if err != nil {
return nil, nil, fmt.Errorf("assembling the mirror's signed note: %w", err)
}
return mirrored, signedNote, nil
}

// Finally, extract the raw cosignature we store in the database.
rawMirrorCosignature, err := cosignature.RawSignature(zeroTimestampMirrorCosignature)
// CosignSubtree returns the mirror's signature over the subtree [start, end)
// from sign-subtree, proven against the mirror's own checkpoint.
//
// TODO(#9038): sign and verify subtrees that do not start at 0.
func (m *MirrorClient) CosignSubtree(ctx context.Context, origin string, start, end int64, subtreeHash tlog.Hash) ([]byte, error) {
if start != 0 {
return nil, fmt.Errorf("subtree [%d, %d) does not start at 0", start, end)
}
mirrored, signedNote, err := m.mirrorCheckpoint(ctx, origin)
if err != nil {
return nil, fmt.Errorf("cosignature: %w", err)
return nil, err
}
if mirrored.Tree.N < end {
return nil, fmt.Errorf("%w: mirror checkpoint at size %d does not include subtree [%d, %d)", ErrSubtreeNotMirrored, mirrored.Tree.N, start, end)
}
if mirrored.Tree.N == end && mirrored.Tree.Hash != subtreeHash {
return nil, fmt.Errorf("mirror checkpoint at size %d has root hash %x, the log's is %x", end, mirrored.Tree.Hash, subtreeHash)
}
Comment on lines +365 to +367

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.

I think we should replace this check with a call to subtree.VerifyConsistency immediately after subtree.ConsistencyProof. That verifies the same property, but for all cases instead of the special case that the subtree lines up with the current tree size.

proof, err := subtree.ConsistencyProof(start, end, mirrored.Tree.N, m.src.hashReaderForTree(ctx, mirrored.Tree))
if err != nil {
return nil, fmt.Errorf("proving subtree [%d, %d) in the mirror's tree of size %d: %s", start, end, mirrored.Tree.N, err)
}
subtreeCosignatureLines, err := m.signSubtree(ctx, start, end, subtreeHash, proof, signedNote)
if err != nil {
return nil, fmt.Errorf("sign-subtree: %w", err)
}

// Verify the mirror's signature. A subtree starting at 0 is signed exactly
// as a checkpoint of its size would be.
noteText, err := (&checkpoint.Checkpoint{Origin: origin, Tree: tlog.Tree{N: end, Hash: subtreeHash}}).Marshal()
if err != nil {
return nil, fmt.Errorf("marshaling the subtree: %w", err)
}
subtreeCosignature, err := m.verifier.FilterByVerify(noteText, subtreeCosignatureLines)
if err != nil {
return nil, fmt.Errorf("subtree signature failed verification: %w", err)
}
if subtreeCosignature.Timestamp != 0 {
return nil, fmt.Errorf("subtree signature has timestamp %d, want 0", subtreeCosignature.Timestamp)
}
return rawMirrorCosignature, nil
return subtreeCosignature.Signature, nil
}
Loading
Loading