@@ -17,7 +17,10 @@ package tessera
1717import (
1818 "bytes"
1919 "context"
20- "encoding/gob"
20+ "crypto/hkdf"
21+ "crypto/hmac"
22+ "crypto/rand"
23+ "crypto/sha256"
2124 "encoding/hex"
2225 "errors"
2326 "fmt"
@@ -42,15 +45,22 @@ var (
4245
4346// MirrorOptions holds mirror lifecycle settings for all storage implementations.
4447type MirrorOptions struct {
45- signer note.Signer
46- cpSource func (context.Context ) ([]byte , error )
48+ signer note.Signer
49+ cpSource func (context.Context ) ([]byte , error )
50+ logVerifier note.Verifier
4751}
4852
4953// NewMirrorOptions creates a new options struct with defaults.
5054func NewMirrorOptions () * MirrorOptions {
5155 return & MirrorOptions {}
5256}
5357
58+ // WithLogVerifier sets the note.Verifier used to verify log checkpoint signatures.
59+ func (o * MirrorOptions ) WithLogVerifier (v note.Verifier ) * MirrorOptions {
60+ o .logVerifier = v
61+ return o
62+ }
63+
5464// WithSigner configures the note.Signer to use when cosigning checkpoints.
5565func (o * MirrorOptions ) WithSigner (s note.Signer ) * MirrorOptions {
5666 o .signer = s
@@ -76,6 +86,9 @@ func (o *MirrorOptions) LeafHasher() func(bundle []byte) (leafHashes [][]byte, e
7686}
7787
7888func (o * MirrorOptions ) valid () error {
89+ if o .logVerifier == nil {
90+ return errors .New ("invalid MirrorOptions: WithLogVerifier must be set" )
91+ }
7992 if o .signer == nil {
8093 return errors .New ("invalid MirrorOptions: WithSigner must be set" )
8194 }
@@ -97,10 +110,11 @@ type MirrorWriter interface {
97110// MirrorTarget is a high-level wrapper that manages the process of mirroring
98111// a source log into a Tessera instance.
99112type MirrorTarget struct {
100- writer MirrorWriter
101- reader LogReader
102- cpSource func (context.Context ) ([]byte , error )
103- signer note.Signer
113+ writer MirrorWriter
114+ reader LogReader
115+ cpSource func (context.Context ) ([]byte , error )
116+ signer note.Signer
117+ ticketKey []byte
104118}
105119
106120// NewMirrorTarget instantiates a new MirrorTarget for the given driver and options.
@@ -122,14 +136,41 @@ func NewMirrorTarget(ctx context.Context, d Driver, opts *MirrorOptions) (*Mirro
122136 if err != nil {
123137 return nil , fmt .Errorf ("failed to init MirrorTarget lifecycle: %v" , err )
124138 }
139+ tK , err := ticketKey (opts .signer .Name (), opts .logVerifier .Name ())
140+ if err != nil {
141+ return nil , fmt .Errorf ("failed to derive ticket key: %v" , err )
142+ }
125143 return & MirrorTarget {
126- writer : mw ,
127- reader : r ,
128- cpSource : opts .cpSource ,
129- signer : opts .signer ,
144+ writer : mw ,
145+ reader : r ,
146+ cpSource : opts .cpSource ,
147+ signer : opts .signer ,
148+ ticketKey : tK ,
130149 }, nil
131150}
132151
152+ // ticketKey derives a unique HMAC key for sealing tickets based on:
153+ // - An ephemeral seed,
154+ // - Identity (origin) of the mirror cosigner,
155+ // - Identity (origin) of the log being mirrored.
156+ //
157+ // It should be called, once, at startup to set the ticket MAC key for the mirror.
158+ //
159+ // TODO(al): We should allow the operator to pass in the seed, so that tickets
160+ // will work across multiple mirror instances and/or restarts.
161+ func ticketKey (mirrorOrigin , logOrigin string ) ([]byte , error ) {
162+ seed := make ([]byte , sha256 .Size )
163+ if _ , err := rand .Read (seed ); err != nil {
164+ return nil , fmt .Errorf ("failed to generate ephemeral seed: %v" , err )
165+ }
166+ // This salt will keep the key unique per mirror, even if the random seed generation above
167+ // were changed to be a "fixed" value provided by the operator.
168+ salt := sha256 .Sum256 (fmt .Appendf (nil , "mirror:\n %s\n " , mirrorOrigin ))
169+ // Bind this key to its usage for MACing tickets for the given log.
170+ info := fmt .Sprintf ("ticket-hmac\n log:\n %s\n " , logOrigin )
171+ return hkdf .Key (sha256 .New , seed , salt [:], info , sha256 .Size )
172+ }
173+
133174// Package represents a single package of entries and its subtree consistency proof.
134175type MirrorPackage struct {
135176 Entries [][]byte
@@ -147,42 +188,43 @@ func (mt *MirrorTarget) AddEntries(ctx context.Context, uploadStart, uploadEnd u
147188 if err != nil {
148189 return 0 , 0 , nil , nil , fmt .Errorf ("failed to read integrated size: %w" , err )
149190 }
150- var t * ticket
151- if t , err = mt .openTicket (ctx , ticketBytes ); err != nil {
152- // Invalid or empty ticket, return a new one.
191+ ticketCP , err := mt .openTicket (ticketBytes )
192+ if err != nil {
193+ slog .DebugContext (ctx , "Invalid Ticket received, returning new one" , slog .Any ("error" , err ), slog .Uint64 ("uploadStart" , uploadStart ), slog .Uint64 ("uploadEnd" , uploadEnd ))
194+
195+ // If the client didn't provide a [valid] ticket, then we don't have a pending
196+ // checkpoint to validate against, so we return a new ticket with the
197+ // current checkpoint.
153198 pendingCP , err := mt .cpSource (ctx )
154199 if err != nil {
155200 return 0 , 0 , nil , nil , fmt .Errorf ("failed to get pending checkpoint: %v" , err )
156201 }
157202 if len (pendingCP ) == 0 {
158203 return 0 , 0 , nil , nil , ErrNoPendingCheckpoint
159204 }
160- t = & ticket {
161- PendingCP : pendingCP ,
162- }
163- ticketBytes , err = mt .sealTicket (ctx , t )
205+ ticketBytes , err = mt .sealTicket (pendingCP )
164206 if err != nil {
165207 return 0 , 0 , nil , nil , fmt .Errorf ("failed to create ticket: %v" , err )
166208 }
167209
168- // If the client didn't provide a [valid] ticket, then we don't have a pending
169- // checkpoint to validate against, so we return a new ticket with the
170- // current checkpoint.
171- _ , pendingSize , _ , err := parse .CheckpointUnsafe (t .PendingCP )
210+ _ , pendingSize , _ , err := parse .CheckpointUnsafe (pendingCP )
172211 if err != nil {
173- slog .ErrorContext (ctx , "Invalid pending checkpoint from source" , slog .String ("pending_checkpoint" , string (t . PendingCP )), slog .String ("error" , err .Error ()))
212+ slog .ErrorContext (ctx , "Invalid pending checkpoint from source" , slog .String ("pending_checkpoint" , string (pendingCP )), slog .String ("error" , err .Error ()))
174213 return 0 , 0 , nil , nil , fmt .Errorf ("failed to parse pending checkpoint while creating ticket: %v" , err )
175214 }
215+
216+ slog .DebugContext (ctx , "Returning new ticket" , slog .Uint64 ("curIntegratedSize" , curIntegratedSize ), slog .Uint64 ("pendingSize" , pendingSize ))
176217 return curIntegratedSize , pendingSize , ticketBytes , nil , ErrConflict
177218 }
178219
179220 var pendingRoot []byte
180- _ , pendingSize , pendingRoot , err = parse .CheckpointUnsafe (t . PendingCP )
221+ _ , pendingSize , pendingRoot , err = parse .CheckpointUnsafe (ticketCP )
181222 if err != nil {
182- slog .ErrorContext (ctx , "Invalid pending checkpoint in ticket" , slog .String ("pending_checkpoint" , string (t . PendingCP )), slog .String ("error" , err .Error ()))
223+ slog .ErrorContext (ctx , "Invalid pending checkpoint in ticket" , slog .String ("pending_checkpoint" , string (ticketCP )), slog .String ("error" , err .Error ()))
183224 return 0 , 0 , nil , nil , fmt .Errorf ("failed to parse pending checkpoint from ticket: %v" , err )
184225 }
185226
227+ slog .DebugContext (ctx , "Valid ticket, proceeding" , slog .Uint64 ("curIntegratedSize" , curIntegratedSize ), slog .Uint64 ("pendingSize" , pendingSize ))
186228 // Handle 409 Conflicts:
187229 // - Zero-request check: If upload_start == 0 and upload_end == 0, the client is
188230 // requesting initial mirror information.
@@ -196,6 +238,7 @@ func (mt *MirrorTarget) AddEntries(ctx context.Context, uploadStart, uploadEnd u
196238 (uploadEnd != pendingSize || uploadEnd < curIntegratedSize ) ||
197239 (uploadStart > curIntegratedSize ) {
198240 // TODO(al): add flexibility about re-writing some entries
241+ slog .ErrorContext (ctx , "Returning conflict" , slog .Uint64 ("curIntegratedSize" , curIntegratedSize ), slog .Uint64 ("pendingSize" , pendingSize ), slog .Uint64 ("uploadStart" , uploadStart ), slog .Uint64 ("uploadEnd" , uploadEnd ))
199242 return curIntegratedSize , pendingSize , ticketBytes , nil , ErrConflict
200243 }
201244
@@ -228,7 +271,7 @@ func (mt *MirrorTarget) AddEntries(ctx context.Context, uploadStart, uploadEnd u
228271 return 0 , 0 , nil , nil , err
229272 case nextEntry == pendingSize :
230273 if ! bytes .Equal (pendingRoot , newRoot ) {
231- slog .ErrorContext (ctx , "CORRUPTION DETECTED - pending root != calculated root" , slog .String ("calculated_root" , hex .EncodeToString (newRoot )), slog .String ("pending_checkpoint" , string (t . PendingCP )))
274+ slog .ErrorContext (ctx , "CORRUPTION DETECTED - pending root != calculated root" , slog .String ("calculated_root" , hex .EncodeToString (newRoot )), slog .String ("pending_checkpoint" , string (ticketCP )))
232275 return 0 , 0 , nil , nil , errors .New ("internal error" )
233276 }
234277 // This is a complete upload.
@@ -238,8 +281,7 @@ func (mt *MirrorTarget) AddEntries(ctx context.Context, uploadStart, uploadEnd u
238281 // - If published, then return the cosig(s) to the caller.
239282 return nextEntry , pendingSize , nil , []byte ("— test cosig\n " ), nil
240283 case nextEntry > pendingSize :
241- // TODO(al): ticket is stale, probably need to update the ticket?
242- slog .WarnContext (ctx , "nextEntry > pendingSize" , slog .Uint64 ("nextEntry" , nextEntry ), slog .Uint64 ("pendingSize" , pendingSize ))
284+ // Ticket is stale, update the ticket
243285 return nextEntry , pendingSize , ticketBytes , nil , nil
244286 default :
245287 // Incomplete upload, return an updated ticket with the current checkpoint.
@@ -252,29 +294,24 @@ func (mt *MirrorTarget) IntegratedSize(ctx context.Context) (uint64, error) {
252294 return mt .reader .IntegratedSize (ctx )
253295}
254296
255- // ticket is the underlying structure of an add-entries ticket.
256- type ticket struct {
257- // PendingCP holds the raw pending checkpoint bytes.
258- PendingCP []byte
297+ func (mt * MirrorTarget ) sealTicket (pendingCP []byte ) ([]byte , error ) {
298+ h := hmac .New (sha256 .New , mt .ticketKey )
299+ h .Write (pendingCP )
300+ mac := h .Sum (nil )
301+ return append (mac , pendingCP ... ), nil
259302}
260303
261- func (mt * MirrorTarget ) sealTicket (ctx context.Context , t * ticket ) ([]byte , error ) {
262- out := bytes.Buffer {}
263- if err := gob .NewEncoder (& out ).Encode (t ); err != nil {
264- return nil , fmt .Errorf ("ticket encoding failed: %v" , err )
304+ func (mt * MirrorTarget ) openTicket (ticketBytes []byte ) ([]byte , error ) {
305+ if len (ticketBytes ) < sha256 .Size {
306+ return nil , errors .New ("invalid ticket" )
265307 }
266- // TODO(al): harden ticket & bind to this particular log mirror.
267- return out .Bytes (), nil
268- }
269308
270- func (mt * MirrorTarget ) openTicket (ctx context.Context , ticketBytes []byte ) (* ticket , error ) {
271- if len (ticketBytes ) == 0 {
272- return nil , errors .New ("empty ticket" )
273- }
274- // TODO(al): harden ticket & verify it's for this particular log mirror.
275- var t ticket
276- if err := gob .NewDecoder (bytes .NewReader (ticketBytes )).Decode (& t ); err != nil {
277- return nil , fmt .Errorf ("ticket decoding failed: %v" , err )
309+ mac , pendingCP := ticketBytes [:sha256 .Size ], ticketBytes [sha256 .Size :]
310+
311+ h := hmac .New (sha256 .New , mt .ticketKey )
312+ h .Write (pendingCP )
313+ if ! hmac .Equal (mac , h .Sum (nil )) {
314+ return nil , errors .New ("invalid ticketMAC" )
278315 }
279- return & t , nil
316+ return pendingCP , nil
280317}
0 commit comments