trees/cosignature: Sign and verify MTC checkpoint cosignatures - #8904
trees/cosignature: Sign and verify MTC checkpoint cosignatures#8904beautifulentropy wants to merge 3 commits into
Conversation
e7a2f71 to
a6beaa1
Compare
jsha
left a comment
There was a problem hiding this comment.
Jumping the review queue to say: looks great to me. Only some comment nits.
| // Cosigner produces cosignatures over checkpoints as an MTC cosigner for a | ||
| // single log: the timestamp is zero, as required for cosignatures used in | ||
| // certificates, and the key is a crypto.Signer so it may be stored in an HSM. | ||
| // Both the CA cosigner and a mirror cosigning the CA's log take this role. |
There was a problem hiding this comment.
Nit: the term "role" as used in the MTC draft is actually to distinguish between different uses of cosigners. So more precisely:
| // Both the CA cosigner and a mirror cosigning the CA's log take this role. | |
| // Both the CA cosigner the mirror cosigner use this. |
(actually looking at https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#name-cosigners it says it only defines the CA cosigner role, and refers out to https://github.com/C2SP/C2SP/blob/main/tlog-mirror.md. But tlog-mirror doesn't use the word "role" anywhere 🤷🏻)
| // "32473.2", nil otherwise. | ||
| func checkRelativeOID(id string) error { | ||
| if id == "" { | ||
| return errors.New("empty") |
There was a problem hiding this comment.
| return errors.New("empty") | |
| return errors.New("empty relative OID") |
| return nil, fmt.Errorf("opening the cosigned note: %s", err) | ||
| } | ||
| // verifier is the only verifier in the list, so every signature in n.Sigs | ||
| // is the cosigner's, verified and length-checked. |
There was a problem hiding this comment.
Nit:
| // is the cosigner's, verified and length-checked. | |
| // is by the verifier's cosigner, verified and length-checked. |
Reasoning: I got a little lost reading "the cosigner" and not seeing a cosigner object in scope. Re-reading the doc comment I see we mention "verifier's cosigner" there - in other words, verifier is always assumed to have a cosigner in this code. IMO makes sense to repeat it here.
| name: oidPrefix + cosignerID, | ||
| origin: origin, |
There was a problem hiding this comment.
Because I have to look it up every time: https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#name-signature-format
cosigner_name and log_origin are computed from the cosigner ID and the issuance log's ID (Section 5.1), respectively.
A CA cosigner is a cosigner (Section 5.3) that certifies the contents of a log. Each CA MUST operate a CA cosigner whose cosigner ID is the same as its CA ID (Section 5.1).
So these are somewhat redundant with each other specifically in the CA cosigner case (since the log_origin is derived from the CA ID). But in the mirror case they are different: log_origin is the log being signed over, which is unrelated to the mirror's cosigner ID.
aarongable
left a comment
There was a problem hiding this comment.
LGTM with some organization nits.
| // marshalCosignedMessage serializes the cosigned.Message for a checkpoint | ||
| // cosignature, with start 0 and end the tree size as the MTC draft section | ||
| // 5.3.1 requires for checkpoints. It rejects a non-positive end. | ||
| func marshalCosignedMessage(name string, timestamp uint64, origin string, end int64, rootHash tlog.Hash) ([]byte, error) { |
There was a problem hiding this comment.
nit: since this is specifically for checkpoint messages, maybe reflect that in the function name.
| // deriving its cosigner name and log origin per mtc-tlog: cosigner ID "32473.2" | ||
| // signs as "oid/1.3.6.1.4.1.32473.2". It errors if either ID is not a dotted | ||
| // decimal OID or signer's public key is not ML-DSA-44. | ||
| func NewCosigner(cosignerID, logID string, signer crypto.Signer) (*Cosigner, error) { |
There was a problem hiding this comment.
Because log IDs must always be caID + ".0." + logNumber, maybe this function should only take the log number as input. Or maybe it should check that the logID has the cosignerID as its prefix?
There was a problem hiding this comment.
See #8904 (comment) - I was confused about this also. Since we'll be using this in our fake mirror also, I think we'll have cosigners where the logID doesn't have the cosignerID as its prefix - the cosignerID will belong to the mirror, while the the logID will belong to the CA.
| // CosignatureLine cosigns the checkpoint described by tree and returns the | ||
| // cosignature as a signature line, trailing newline included. For a | ||
| // timestamped_signature, use CosignCheckpoint. | ||
| func (c *Cosigner) CosignatureLine(tree tlog.Tree) (string, error) { |
There was a problem hiding this comment.
I don't think this necessarily needs to be in this package. Per the PR description, the only caller of this is going to be the test mirror, so it doesn't necessarily need to be in a shared package. And this is the only caller of signatureLineFor, so that helper could be inlined into the test mirror as well. Since this is effectively test-only code, I think keeping it outside the general package makes sense.
| // is not a dotted decimal OID. | ||
| // | ||
| // https://c2sp.org/mtc-tlog | ||
| func Origin(logID string) (string, error) { |
There was a problem hiding this comment.
I think this doesn't need to be exported.
If it were needed by other packages, then I think this wouldn't be the right home for it. Having some random package call cosignature.Origin(log.OID) feels wrong, it seems like it should be in a log package or something like that.
But I think it isn't actually needed by other packages, because you expose it as Cosigner.Origin(). For example, the usage in your followup PR can be replaced by swapping the order of those two stanzas and then calling cosigner.Origin().
Implement ML-DSA-44 cosignatures over log checkpoints. tlog-cosignature specifies the cosignature format, and mtc-tlog specifies how MTC uses it. This package is written to both: it derives cosigner names and log origins as mtc-tlog requires, accepts only ML-DSA-44 keys, and signs with a zero timestamp, which section 6.2 of the MTC draft requires for cosignatures used in certificates.
Export the following:
Cosignercosigns checkpoints for a single log, through acrypto.Signerso the key may be stored in an HSM..CosignCheckpoint()returns the cosignature as atimestamped_signature, which is how the MTCA signs the checkpoints it stores, and.CosignatureLine()performs the same signing but returns the cosignature as a note signature line, which is what a mirror hands back to themtpublisher.Verifieris anote.Verifierfor a cosigner's public key..Verify()satisfies the interface, checking a cosignature against checkpoint note text, and.VerifyCheckpoint()checks one against an already parsed origin and tree, which is how the MTCA verifies each cosignature before storing it.Originderives a log origin from a log ID.TimestampedSignatureverifies a signature line against checkpoint note text and returns the cosigner'stimestamped_signature, which is how themtpublisherturns a mirror's line into the bytes it stores.RawSignaturereturns the bare ML-DSA-44 signature necessary for anMTCSignature, refusing non-zero timestamps. The MTCA and mtpublisher will both have atimestamped_signaturein hand, they will use this to extract a raw signature to store in the database.