@@ -23,6 +23,7 @@ import (
2323
2424 "golang.org/x/crypto/cryptobyte"
2525 "golang.org/x/mod/sumdb/note"
26+ "golang.org/x/mod/sumdb/tlog"
2627)
2728
2829const (
@@ -40,7 +41,7 @@ func NewCosignatureSigner(name string, key crypto.Signer) (*CosignatureSigner, e
4041 if err != nil {
4142 return nil , err
4243 }
43- s := & CosignatureSigner {v : * v }
44+ s := & CosignatureSigner {v : * v , key : key }
4445 switch pubKey .(type ) {
4546 case ed25519.PublicKey :
4647 s .sign = func (msg []byte ) ([]byte , error ) {
@@ -104,18 +105,6 @@ func formatCosignatureV1(t uint64, msg []byte) ([]byte, error) {
104105}
105106
106107func formatSubtreeV1 (name string , t uint64 , msg []byte ) ([]byte , error ) {
107- // The signed message is in the following format
108- //
109- // struct {
110- // uint8 label[12] = "subtree/v1\n\0";
111- // opaque cosigner_name<1..2^8-1>;
112- // uint64 timestamp;
113- // opaque log_origin<1..2^8-1>;
114- // uint64 start;
115- // uint64 end;
116- // uint8 hash[32];
117- // } cosigned_message;
118-
119108 c , err := ParseCheckpoint (string (msg ))
120109 if err != nil {
121110 return nil , fmt .Errorf ("message being signed is not a valid checkpoint: %w" , err )
@@ -128,18 +117,46 @@ func formatSubtreeV1(name string, t uint64, msg []byte) ([]byte, error) {
128117 if string (msg ) != c .String () {
129118 return nil , errors .New ("message being signed does not match parsed checkpoint" )
130119 }
120+ return subtreeCosignedMessage (name , t , c .Origin , 0 , c .N , c .Hash )
121+ }
122+
123+ func subtreeCosignedMessage (name string , t uint64 , origin string , start , end int64 , hash tlog.Hash ) ([]byte , error ) {
124+ // The signed message is in the following format
125+ //
126+ // struct {
127+ // uint8 label[12] = "subtree/v1\n\0";
128+ // opaque cosigner_name<1..2^8-1>;
129+ // uint64 timestamp;
130+ // opaque log_origin<1..2^8-1>;
131+ // uint64 start;
132+ // uint64 end;
133+ // uint8 hash[32];
134+ // } cosigned_message;
135+
131136 b := & cryptobyte.Builder {}
132137 b .AddBytes ([]byte ("subtree/v1\n \x00 " ))
138+ if len (name ) == 0 || len (name ) > 255 {
139+ return nil , errors .New ("cosigner name must be 1-255 bytes" )
140+ }
133141 b .AddUint8LengthPrefixed (func (b * cryptobyte.Builder ) {
134142 b .AddBytes ([]byte (name ))
135143 })
144+ if t > math .MaxInt64 {
145+ return nil , errors .New ("timestamp is too large" )
146+ }
147+ if t != 0 && start != 0 {
148+ return nil , errors .New ("timestamp must be zero for non-root subtrees" )
149+ }
136150 b .AddUint64 (t )
151+ if len (origin ) == 0 || len (origin ) > 255 {
152+ return nil , errors .New ("log origin must be 1-255 bytes" )
153+ }
137154 b .AddUint8LengthPrefixed (func (b * cryptobyte.Builder ) {
138- b .AddBytes ([]byte (c . Origin ))
155+ b .AddBytes ([]byte (origin ))
139156 })
140- b .AddUint64 (0 )
141- b .AddUint64 (uint64 (c . N ))
142- b .AddBytes (c . Hash [:])
157+ b .AddUint64 (uint64 ( start ) )
158+ b .AddUint64 (uint64 (end ))
159+ b .AddBytes (hash [:])
143160 return b .Bytes ()
144161}
145162
@@ -148,6 +165,7 @@ func formatSubtreeV1(name string, t uint64, msg []byte) ([]byte, error) {
148165type CosignatureSigner struct {
149166 v CosignatureVerifier
150167 sign func ([]byte ) ([]byte , error )
168+ key crypto.Signer
151169}
152170
153171func (s * CosignatureSigner ) Name () string { return s .v .Name () }
@@ -157,6 +175,37 @@ func (s *CosignatureSigner) Verifier() *CosignatureVerifier { return &s.v }
157175
158176var _ note.Signer = & CosignatureSigner {}
159177
178+ // SignSubtree signs a subtree [start, end) with the given hash for the log with
179+ // the given origin. The timestamp is set to zero. The returned signature is in
180+ // the format of a note signature, starting with the — and ending with a newline.
181+ func (s * CosignatureSigner ) SignSubtree (origin string , start , end int64 , hash tlog.Hash ) ([]byte , error ) {
182+ if _ , ok := s .v .PublicKey ().(* mldsa.PublicKey ); ! ok {
183+ return nil , errors .New ("subtree signatures are only supported for ML-DSA-44 keys" )
184+ }
185+ if ! ValidSubtree (start , end ) {
186+ return nil , errors .New ("invalid subtree" )
187+ }
188+
189+ m , err := subtreeCosignedMessage (s .Name (), 0 , origin , start , end , hash )
190+ if err != nil {
191+ return nil , err
192+ }
193+
194+ ss , err := s .key .Sign (nil , m , crypto .Hash (0 ))
195+ if err != nil {
196+ return nil , err
197+ }
198+
199+ // key hash || timestamp || signature.
200+ sig := make ([]byte , 0 , 4 + 8 + mldsa .MLDSA44SignatureSize )
201+ sig = binary .BigEndian .AppendUint32 (sig , s .KeyHash ())
202+ sig = binary .BigEndian .AppendUint64 (sig , 0 )
203+ sig = append (sig , ss ... )
204+
205+ res := "— " + s .Name () + " " + base64 .StdEncoding .EncodeToString (sig ) + "\n "
206+ return []byte (res ), nil
207+ }
208+
160209// CosignatureVerifier is a [note.Verifier] that verifies cosignatures
161210// according to c2sp.org/tlog-cosignature.
162211type CosignatureVerifier struct {
@@ -281,6 +330,59 @@ func (v *CosignatureVerifier) PublicKey() crypto.PublicKey {
281330 return v .key
282331}
283332
333+ // VerifySubtree reports whether signature is a valid cosignature by this
334+ // verifier over the subtree [start, end) with the given hash for the log with
335+ // the given origin.
336+ //
337+ // signature must be a single note signature line ending in a newline, like the
338+ // one returned by [CosignatureSigner.SignSubtree], and its key name and hash
339+ // must match this verifier.
340+ //
341+ // Note that a checkpoint cosignature is a valid cosignature over the equivalent
342+ // subtree, and this method allows non-zero timestamps for root subtrees.
343+ func (v * CosignatureVerifier ) VerifySubtree (origin string , start , end int64 , hash tlog.Hash , signature []byte ) bool {
344+ k , ok := v .key .(* mldsa.PublicKey )
345+ if ! ok {
346+ return false
347+ }
348+ if ! ValidSubtree (start , end ) {
349+ return false
350+ }
351+
352+ line , ok := strings .CutSuffix (string (signature ), "\n " )
353+ if ! ok {
354+ return false
355+ }
356+ line , ok = strings .CutPrefix (line , "— " )
357+ if ! ok {
358+ return false
359+ }
360+ name , b64 , _ := strings .Cut (line , " " )
361+ sig , err := base64 .StdEncoding .DecodeString (b64 )
362+ if err != nil || b64 == "" || len (sig ) < 4 {
363+ return false
364+ }
365+ if name != v .name || binary .BigEndian .Uint32 (sig ) != v .hash {
366+ return false
367+ }
368+ sig = sig [4 :]
369+
370+ if len (sig ) != 8 + mldsa .MLDSA44SignatureSize {
371+ return false
372+ }
373+ t := binary .BigEndian .Uint64 (sig )
374+ sig = sig [8 :]
375+ // If start is not zero, the timestamp must be zero.
376+ if t > math .MaxInt64 || (start != 0 && t != 0 ) {
377+ return false
378+ }
379+ m , err := subtreeCosignedMessage (v .name , t , origin , start , end , hash )
380+ if err != nil {
381+ return false
382+ }
383+ return mldsa .Verify (k , m , sig , nil ) == nil
384+ }
385+
284386// String returns the vkey encoding of the verifier, according to
285387// c2sp.org/signed-note.
286388func (v * CosignatureVerifier ) String () string {
0 commit comments