Visitar URL original
Align our MTC implementation with draft-07 by beautifulentropy · Pull Request #9134 · letsencrypt/boulder · GitHub
Skip to content

Align our MTC implementation with draft-07 - #9134

Open
beautifulentropy wants to merge 9 commits into
mainfrom
mtc-draft07-alignment
Open

beautifulentropy wants to merge 9 commits into
mainfrom
mtc-draft07-alignment

Conversation

@beautifulentropy

@beautifulentropy beautifulentropy commented Oct 29, 2026 •

Copy link
Copy Markdown
Member
  • proof: Use id-alg-mtcProof, 1.3.6.1.5.5.7.6.67, which draft 07 allocated, in place of the experimental 1.3.6.1.4.1.44363.47.0.
  • subtree: Accept empty subtrees, [x, x), per section 4.1 (introduced in draft 06). ConsistencyProof returns an empty proof for them. The appendix C.1 digests move to their new values, the C.2.1 validity cases are added, and mirror.SignSubtreeRequest uses subtree.Valid.
  • trees: Rename cosigned.Message to cosigned.Subtree and proof.SubtreeSignature to proof.Cosignature, matching the renamed CosignedSubtree and Cosignature structures, and timestamped_signature to TimestampedSignature in comments.
  • core: Cap the serial index at trees.MaxSize-1, since a log holds at most 2^48-1 entries per section 5.2.
  • linter: Build the test issuer with id-rdna-trustAnchorID and a RELATIVE-OID value, removing the last use of the experimental attribute.
  • cosignature: Build checkpoint and subtree messages with separate helpers, so a checkpoint message always starts at zero and a subtree message never carries a timestamp, as section 5.3.1 requires. Addresses mtpublisher: Store the mirror's checkpoint line and subtree signatures #9097 (comment).
  • trees: Reject non-canonical base64 in the mirror-info ticket and signature line decoders, as tlog-mirror v0.1.0 and signed-note v1.1.0 require.
  • trees: Validate checkpoint origins and cosigner names against tlog-cosignature v1.1.0's cosigner name construction. Checkpoints whose origin contains a space or a plus sign are now rejected.

Part of #9082

@beautifulentropy
beautifulentropy requested a review from a team as a code owner October 29, 2026 16:47
@beautifulentropy
beautifulentropy requested a review from jsha October 29, 2026 16:47
@beautifulentropy beautifulentropy changed the title mtc: Align the implementation with draft-ietf-plants-merkle-tree-certs-07 Align our MTC implementation with draft-ietf-plants-merkle-tree-certs-07 Oct 29, 2026
@beautifulentropy

beautifulentropy commented Oct 29, 2026 •

Copy link
Copy Markdown
Member Author

Apologies for the force-push. I missed some bullets on one of the Claude reviews. I reviewed the findings and pushed up the necessary changes, and updated the PR description.

@beautifulentropy beautifulentropy changed the title Align our MTC implementation with draft-ietf-plants-merkle-tree-certs-07 Align our MTC implementation with draft-07 Oct 29, 2026
Comment thread trees/trees.go
// construction in https://c2sp.org/tlog-cosignature#cosigners, which log
// origins also follow: a non-empty UTF-8 string of at most 255 bytes with no
// Unicode spaces, plus signs, or control characters below U+0020.
func CheckName(name string) error {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I kind of went back and forth on the correct home for this and couldn't make a decision if y'all have better ideas feel free to chime in.

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 somewhere in the cosignature package, e.g. cosignature.ValidCosignerName (a mouthful I know). My rationale for there, even though this is shared by log origins, is that a log origin is also a cosigner. Also it's fine here. 🤷🏻

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.

The requirements for this are defined by the C2SP tlog-cosignature doc, therefore the enforcement of it belongs in the cosignature package, IMO.

(In general, I think it would be nice for future maintenance purposes if we had a very clean mapping of source packages to C2SP documents -- one that implements signed-note, one that implements tlog-cosignature, one that implements tlog-checkpoint, etc.)

jsha
jsha previously approved these changes Oct 29, 2026

@aarongable aarongable left a comment

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.

core: Cap the serial index at trees.MaxSize-1, since a log holds at most 2^48-1 entries per section 5.2.

It took me like four reads to understand this correctly. It seems like it is saying "cap the serial index to 2^48-1", because both mathematical statements have a -1 in them. What it's actually saying is that trees.MaxSize is 2^48-1, and our max index needs to be one less than that (i.e. 2^48-2) because entries are zero-indexed.

I suggest rephrasing as:

core: Cap the serial index at trees.MaxSize-1, rather than recomputing the max serial index directly and getting an off-by-one error

Comment thread linter/linter_test.go
Comment on lines +62 to +66
caID, err := reloid.FromString("44947.4.2.1")
if err != nil {
t.Fatal(err)
}
idRDNATrustAnchorID := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 25, 3}

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.

completely optional hyper-nit: idRDNATrustAnchorID is used before caID is, so create it first as well

Suggested change
caID, err := reloid.FromString("44947.4.2.1")
if err != nil {
t.Fatal(err)
}
idRDNATrustAnchorID := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 25, 3}
idRDNATrustAnchorID := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 25, 3}
caID, err := reloid.FromString("44947.4.2.1")
if err != nil {
t.Fatal(err)
}

Comment thread trees/trees.go
// construction in https://c2sp.org/tlog-cosignature#cosigners, which log
// origins also follow: a non-empty UTF-8 string of at most 255 bytes with no
// Unicode spaces, plus signs, or control characters below U+0020.
func CheckName(name string) error {

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.

The requirements for this are defined by the C2SP tlog-cosignature doc, therefore the enforcement of it belongs in the cosignature package, IMO.

(In general, I think it would be nice for future maintenance purposes if we had a very clean mapping of source packages to C2SP documents -- one that implements signed-note, one that implements tlog-cosignature, one that implements tlog-checkpoint, etc.)

Comment on lines 338 to +339
// TestCosignSubtree checks that a subtree starting at 0 is signed as a
// checkpoint of its size is, and that a later subtree verifies over its own
// message.
// checkpoint of its size is, that a later subtree verifies over its own

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 the first part of this comment is no longer true -- a subtree starting at zero is not signed the same way as a checkpoint of the same size, because the checkpoint includes a timestamp.


// marshalCheckpointMessage serializes the cosigned.Message for a checkpoint
// marshalCheckpointMessage serializes the cosigned.Subtree for a checkpoint
// cosignature, with start 0 and end the tree size as the MTC draft section

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.

"and end the tree size" both reads poorly and doesn't seem relevant here, given that the input to this function is end, not treeSize.

Comment thread trees/cosigned/message.go
Comment on lines +29 to +30
// It errors if cosigner_name or log_origin are not valid names. It does not validate semantic constraints,
// like [start, end) being a valid subtree.

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 it would just be a single call to subtree.Valid to do this now.

Comment thread trees/cosigned/message.go

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.

One message->subtree renaming you didn't do is the name of this file. Unsure whether that rename is worth it or not.

Comment thread trees/subtree/subtree.go
// above x, an exclusive ceiling. BIT_CEIL(x) is inclusive, the smallest
// power of two at least x, so we apply it to end-start-1.
bitCeil := uint64(1) << bits.Len64(uint64(end-start-1)) //nolint:gosec // G115: the start >= end check above leaves end-start positive, so end-start-1 is non-negative.
bitCeil := uint64(1) << bits.Len64(uint64(end-start-1)) //nolint:gosec // G115: the start == end return above leaves end-start positive, so end-start-1 is non-negative.

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.

Suggested change
bitCeil := uint64(1) << bits.Len64(uint64(end-start-1)) //nolint:gosec // G115: the start == end return above leaves end-start positive, so end-start-1 is non-negative.
bitCeil := uint64(1) << bits.Len64(uint64(end-start-1)) //nolint:gosec // G115: the checks above mean that end-start is positive, so end-start-1 is non-negative.

Comment on lines +454 to +457
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
nonCanonical := bytes.Clone(line)
last := bytes.IndexByte(nonCanonical, '=') - 1
nonCanonical[last] = alphabet[strings.IndexByte(alphabet, nonCanonical[last])+1]

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.

A few comments here:

  1. It took me a while to figure out exactly what's going on here; there should be a comment explaining it (like there is for the similar code down on line 607).
  2. If line is a length that doesn't require padding characters, this will fail: last will be set to -2, and indexing nonCanonical[-2] will panic.
  3. If line ends with .../=, then this will fail: alphabet[36] will panic.

Comments 2 and 3 also apply down on line 607.

Given that rejecting non-canonical encodings is happens very early in VerifySubtreeSignatureLine, I think it would be much cleaner to just hard-code a base64 string literal that has one or more padding bits set to 1. Something like TWF= (from the wikipedia examples) should work.

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.

3 participants