@@ -24,6 +24,12 @@ import (
2424 "time"
2525
2626 "filippo.io/age"
27+ "filippo.io/torchwood"
28+ "filippo.io/torchwood/tesserax"
29+ "github.com/transparency-dev/tessera"
30+ "github.com/transparency-dev/tessera/storage/posix"
31+ "golang.org/x/mod/sumdb/note"
32+ "golang.org/x/mod/sumdb/tlog"
2733 "golang.org/x/net/http2"
2834 "golang.org/x/net/http2/h2c"
2935 "zombiezen.com/go/sqlite"
3541 embeddedFS embed.FS
3642
3743 dbPath = flag .String ("db" , "keyserver.sqlite3" , "path to SQLite database" )
44+ logPath = flag .String ("logdir" , "keyserver-tlog" , "directory for transparency log" )
3845 listenAddr = flag .String ("listen" , "localhost:13889" , "address to listen on" )
3946)
4047
@@ -43,11 +50,16 @@ type Server struct {
4350 templates * template.Template
4451 hmacKey []byte
4552 baseURL string
53+ reader tessera.LogReader
54+ appender * tessera.Appender
55+ awaiter * tessera.PublicationAwaiter
56+ verifier note.Verifier
4657}
4758
4859type KeyData struct {
4960 Pubkey string `json:"pubkey"`
5061 UpdatedAt int64 `json:"updated_at"`
62+ LogIndex int64 `json:"log_index"`
5163}
5264
5365const (
@@ -61,6 +73,33 @@ const (
6173
6274func main () {
6375 flag .Parse ()
76+ ctx := context .Background ()
77+
78+ s , err := note .NewSigner (os .Getenv ("LOG_KEY" ))
79+ if err != nil {
80+ log .Fatalln ("failed to create checkpoint signer:" , err )
81+ }
82+ verifier , err := torchwood .NewVerifierFromSigner (os .Getenv ("LOG_KEY" ))
83+ if err != nil {
84+ log .Fatalln ("failed to create checkpoint verifier:" , err )
85+ }
86+
87+ driver , err := posix .New (ctx , posix.Config {
88+ Path : * logPath ,
89+ })
90+ if err != nil {
91+ log .Fatalln ("failed to create log storage driver:" , err )
92+ }
93+
94+ appender , shutdown , logReader , err := tessera .NewAppender (ctx , driver , tessera .NewAppendOptions ().
95+ WithCheckpointSigner (s ).
96+ WithBatching (256 , 250 * time .Millisecond ).
97+ WithCheckpointInterval (1 * time .Second ))
98+ if err != nil {
99+ log .Fatalln ("failed to create log appender:" , err )
100+ }
101+ defer shutdown (context .Background ())
102+ awaiter := tessera .NewPublicationAwaiter (ctx , logReader .ReadCheckpoint , 250 * time .Millisecond )
64103
65104 // Check for development vs production mode
66105 postmarkToken := os .Getenv ("POSTMARK_TOKEN" )
@@ -111,6 +150,10 @@ func main() {
111150 templates : templates ,
112151 hmacKey : hmacKey ,
113152 baseURL : baseURL ,
153+ reader : logReader ,
154+ appender : appender ,
155+ awaiter : awaiter ,
156+ verifier : verifier ,
114157 }
115158
116159 // Set up routes
@@ -129,6 +172,10 @@ func main() {
129172 }
130173 mux .Handle ("GET /static/" , http .StripPrefix ("/static/" , http .FileServer (http .FS (staticFS ))))
131174
175+ // Serve tlog-tiles log
176+ fs := http .StripPrefix ("/tlog/" , http .FileServer (http .Dir (* logPath )))
177+ mux .Handle ("GET /tlog/" , fs )
178+
132179 // Start server with h2c support
133180 log .Println ("" )
134181 log .Printf ("Starting age Keyserver on %s" , * listenAddr )
@@ -183,6 +230,14 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
183230 http .Error (w , "Email is required" , http .StatusBadRequest )
184231 return
185232 }
233+ // Emails are technically case sensitive, but users are unlikely to monitor
234+ // all case variations, so we normalize to lowercase. We do ti before
235+ // sending the logic link, so normalization can't lead to impersonation.
236+ email = strings .ToLower (email )
237+ if strings .ContainsAny (email , "\n " ) {
238+ http .Error (w , "Invalid email format" , http .StatusBadRequest )
239+ return
240+ }
186241
187242 // Verify captcha
188243 if ! verifyCaptcha (captchaResponse ) {
@@ -276,18 +331,37 @@ func (s *Server) handleSetKey(w http.ResponseWriter, r *http.Request) {
276331 }
277332
278333 // Validate age public key
334+ var proof string
279335 if pubkey != "" {
280336 if _ , err := age .ParseX25519Recipient (pubkey ); err != nil {
281337 http .Error (w , "Invalid age public key format" , http .StatusBadRequest )
282338 return
283339 }
284340
341+ // Add to transparency log
342+ entry := tessera .NewEntry (fmt .Appendf (nil , "%s\n %s\n " , email , pubkey ))
343+ index , _ , err := s .awaiter .Await (r .Context (), s .appender .Add (r .Context (), entry ))
344+ if err != nil {
345+ http .Error (w , "Failed to add to transparency log" , http .StatusInternalServerError )
346+ log .Printf ("transparency log error: %v" , err )
347+ return
348+ }
349+
285350 // Store in database
286- if err := s .storeKey (email , pubkey ); err != nil {
351+ if err := s .storeKey (email , pubkey , int64 ( index . Index ) ); err != nil {
287352 http .Error (w , "Failed to store key" , http .StatusInternalServerError )
288353 log .Printf ("database error: %v" , err )
289354 return
290355 }
356+
357+ // Generate proof for success page
358+ proofBytes , err := s .makeSpicySignature (r .Context (), int64 (index .Index ))
359+ if err != nil {
360+ http .Error (w , "Failed to create proof" , http .StatusInternalServerError )
361+ log .Printf ("proof error: %v" , err )
362+ return
363+ }
364+ proof = string (proofBytes )
291365 } else {
292366 // Delete key
293367 if err := s .deleteKey (email ); err != nil {
@@ -301,6 +375,7 @@ func (s *Server) handleSetKey(w http.ResponseWriter, r *http.Request) {
301375 if err := s .templates .ExecuteTemplate (w , "success.html" , map [string ]string {
302376 "Email" : email ,
303377 "Pubkey" : pubkey ,
378+ "Proof" : proof ,
304379 }); err != nil {
305380 http .Error (w , "Internal server error" , http .StatusInternalServerError )
306381 log .Printf ("template error: %v" , err )
@@ -325,14 +400,43 @@ func (s *Server) handleLookup(w http.ResponseWriter, r *http.Request) {
325400 return
326401 }
327402
403+ proof , err := s .makeSpicySignature (r .Context (), data .LogIndex )
404+ if err != nil {
405+ http .Error (w , "Failed to create proof" , http .StatusInternalServerError )
406+ log .Printf ("proof error: %v" , err )
407+ return
408+ }
409+
328410 // Return as JSON
329411 w .Header ().Set ("Content-Type" , "application/json" )
330412 json .NewEncoder (w ).Encode (map [string ]string {
331413 "email" : email ,
332414 "pubkey" : data .Pubkey ,
415+ "proof" : string (proof ),
333416 })
334417}
335418
419+ func (s * Server ) makeSpicySignature (ctx context.Context , index int64 ) ([]byte , error ) {
420+ checkpoint , err := s .reader .ReadCheckpoint (ctx )
421+ if err != nil {
422+ return nil , fmt .Errorf ("failed to read checkpoint: %v" , err )
423+ }
424+ n , err := note .Open (checkpoint , note .VerifierList (s .verifier ))
425+ if err != nil {
426+ return nil , fmt .Errorf ("failed to open checkpoint note: %v" , err )
427+ }
428+ c , err := torchwood .ParseCheckpoint (n .Text )
429+ if err != nil {
430+ return nil , fmt .Errorf ("failed to parse checkpoint: %v" , err )
431+ }
432+ p , err := tlog .ProveRecord (c .N , index , torchwood .TileHashReaderWithContext (
433+ ctx , c .Tree , tesserax .NewTileReader (s .reader )))
434+ if err != nil {
435+ return nil , fmt .Errorf ("failed to create proof: %v" , err )
436+ }
437+ return torchwood .FormatProof (index , p , checkpoint ), nil
438+ }
439+
336440func (s * Server ) generateHMAC (email string , ts int64 ) string {
337441 msg := fmt .Sprintf ("%s:%d" , email , ts )
338442 h := hmac .New (sha256 .New , s .hmacKey )
@@ -403,10 +507,11 @@ func (s *Server) getKeyData(email string) (*KeyData, error) {
403507 return & data , nil
404508}
405509
406- func (s * Server ) storeKey (email , pubkey string ) error {
510+ func (s * Server ) storeKey (email , pubkey string , index int64 ) error {
407511 data := KeyData {
408512 Pubkey : pubkey ,
409513 UpdatedAt : time .Now ().Unix (),
514+ LogIndex : index ,
410515 }
411516
412517 jsonData , err := json .Marshal (data )
0 commit comments