Skip to content

Commit 4d69bd1

Browse files
committed
cmd/age-keyserver: hash age public key to prevent log poisoning
1 parent 0d20b2c commit 4d69bd1

2 files changed

Lines changed: 91 additions & 17 deletions

File tree

cmd/age-keylookup/main.go

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package main
22

33
import (
4+
"bytes"
45
"context"
6+
"crypto/sha256"
57
"encoding/base64"
68
"encoding/json"
79
"flag"
@@ -157,8 +159,9 @@ func lookupKey(serverURL string, v note.Verifier, vrfKey *vrf.PublicKey, email s
157159
}
158160

159161
// Verify spicy signature
160-
vrfHashB64 := base64.StdEncoding.EncodeToString(vrfHash)
161-
entry := fmt.Appendf(nil, "%s\n%s\n", vrfHashB64, result.Pubkey)
162+
h := sha256.New()
163+
h.Write([]byte(result.Pubkey))
164+
entry := h.Sum(vrfHash) // vrf-r255(email) || SHA-256(pubkey)
162165
if err := torchwood.VerifyProof(v.Name(), func(b []byte) (*note.Note, error) {
163166
return note.Open(b, note.VerifierList(v))
164167
}, tlog.RecordHash(entry), []byte(result.Proof)); err != nil {
@@ -169,7 +172,7 @@ func lookupKey(serverURL string, v note.Verifier, vrfKey *vrf.PublicKey, email s
169172
}
170173

171174
func monitorLog(serverURL string, v note.Verifier, vrfKey *vrf.PublicKey, email string) ([]string, error) {
172-
// Request the VRF proof from the monitor endpoint
175+
// Request the VRF proof and history from the monitor endpoint
173176
monitorURL := serverURL + "/api/monitor?email=" + url.QueryEscape(email)
174177
client := &http.Client{
175178
Timeout: 10 * time.Second,
@@ -187,8 +190,9 @@ func monitorLog(serverURL string, v note.Verifier, vrfKey *vrf.PublicKey, email
187190
return nil, fmt.Errorf("keyserver error: %s - %s", resp.Status, string(body))
188191
}
189192
var result struct {
190-
Email string `json:"email"`
191-
VRFProof []byte `json:"vrf_proof"`
193+
Email string `json:"email"`
194+
VRFProof []byte `json:"vrf_proof"`
195+
History []string `json:"history"`
192196
}
193197
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
194198
return nil, fmt.Errorf("failed to parse response: %w", err)
@@ -197,6 +201,13 @@ func monitorLog(serverURL string, v note.Verifier, vrfKey *vrf.PublicKey, email
197201
return nil, fmt.Errorf("keyserver returned unexpected email: %q", result.Email)
198202
}
199203

204+
// Prepare map of hashes of historical keys
205+
historyHashes := make(map[[32]byte]string)
206+
for _, pk := range result.History {
207+
h := sha256.Sum256([]byte(pk))
208+
historyHashes[h] = pk
209+
}
210+
200211
// Compute and verify VRF hash
201212
vrfProof, err := vrf.NewProof(result.VRFProof)
202213
if err != nil {
@@ -233,17 +244,17 @@ func monitorLog(serverURL string, v note.Verifier, vrfKey *vrf.PublicKey, email
233244
// Fetch all entries up to the checkpoint size
234245
var pubkeys []string
235246
for i, entry := range c.AllEntries(context.Background(), checkpoint.Tree, 0) {
236-
e, rest, ok := strings.Cut(string(entry), "\n")
237-
if !ok {
238-
return nil, fmt.Errorf("malformed log entry %d: %q", i, string(entry))
247+
if len(entry) != 64+32 {
248+
return nil, fmt.Errorf("invalid entry size at index %d", i)
239249
}
240-
k, rest, ok := strings.Cut(rest, "\n")
241-
if !ok || rest != "" {
242-
return nil, fmt.Errorf("malformed log entry %d: %q", i, string(entry))
250+
if !bytes.Equal(entry[:64], vrfHash) {
251+
continue
243252
}
244-
if e == base64.StdEncoding.EncodeToString(vrfHash) {
245-
pubkeys = append(pubkeys, k)
253+
pk, ok := historyHashes[([32]byte)(entry[64:])]
254+
if !ok {
255+
return nil, fmt.Errorf("found unknown public key hash in log at index %d", i)
246256
}
257+
pubkeys = append(pubkeys, pk)
247258
}
248259
if c.Err() != nil {
249260
return nil, fmt.Errorf("error fetching log entries: %w", c.Err())

cmd/age-keyserver/main.go

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ const (
7171
CREATE TABLE IF NOT EXISTS keys (
7272
email TEXT PRIMARY KEY,
7373
json_data BLOB
74-
) STRICT;`
74+
) STRICT;
75+
CREATE TABLE IF NOT EXISTS history (
76+
email TEXT NOT NULL,
77+
pubkey TEXT NOT NULL
78+
) STRICT;
79+
CREATE INDEX IF NOT EXISTS history_email_idx ON history(email);
80+
`
7581
)
7682

7783
func main() {
@@ -131,7 +137,7 @@ func main() {
131137
dbpool, err := sqlitex.NewPool(*dbPath, sqlitex.PoolOptions{
132138
PoolSize: 10,
133139
PrepareConn: func(conn *sqlite.Conn) error {
134-
return sqlitex.ExecuteTransient(conn, schema, nil)
140+
return sqlitex.ExecScript(conn, schema)
135141
},
136142
})
137143
if err != nil {
@@ -354,10 +360,18 @@ func (s *Server) handleSetKey(w http.ResponseWriter, r *http.Request) {
354360

355361
// Compute VRF hash and proof
356362
vrfProof := s.vrf.Prove([]byte(email))
357-
vrfHash := base64.StdEncoding.EncodeToString(vrfProof.Hash())
363+
364+
// Keep track of the unhashed key
365+
if err := s.storeHistory(email, pubkey); err != nil {
366+
http.Error(w, "Failed to store key history", http.StatusInternalServerError)
367+
log.Printf("database error: %v", err)
368+
return
369+
}
358370

359371
// Add to transparency log
360-
entry := tessera.NewEntry(fmt.Appendf(nil, "%s\n%s\n", vrfHash, pubkey))
372+
h := sha256.New()
373+
h.Write([]byte(pubkey))
374+
entry := tessera.NewEntry(h.Sum(vrfProof.Hash())) // vrf-r255(email) || SHA-256(pubkey)
361375
index, _, err := s.awaiter.Await(r.Context(), s.appender.Add(r.Context(), entry))
362376
if err != nil {
363377
http.Error(w, "Failed to add to transparency log", http.StatusInternalServerError)
@@ -441,11 +455,19 @@ func (s *Server) handleMonitor(w http.ResponseWriter, r *http.Request) {
441455
return
442456
}
443457

458+
history, err := s.getHistory(email)
459+
if err != nil {
460+
http.Error(w, "Database error", http.StatusInternalServerError)
461+
log.Printf("database error: %v", err)
462+
return
463+
}
464+
444465
// Return as JSON
445466
w.Header().Set("Content-Type", "application/json")
446467
json.NewEncoder(w).Encode(map[string]any{
447468
"email": email,
448469
"vrf_proof": s.vrf.Prove([]byte(email)).Bytes(),
470+
"history": history,
449471
})
450472
}
451473

@@ -581,6 +603,47 @@ func (s *Server) deleteKey(email string) error {
581603
})
582604
}
583605

606+
func (s *Server) getHistory(email string) ([]string, error) {
607+
conn, err := s.dbpool.Take(context.Background())
608+
if err != nil {
609+
return nil, err
610+
}
611+
defer s.dbpool.Put(conn)
612+
613+
var pubkeys []string
614+
err = sqlitex.Execute(conn, `
615+
SELECT pubkey FROM history
616+
WHERE email = ?
617+
`, &sqlitex.ExecOptions{
618+
Args: []any{email},
619+
ResultFunc: func(stmt *sqlite.Stmt) error {
620+
pubkey := stmt.ColumnText(0)
621+
pubkeys = append(pubkeys, pubkey)
622+
return nil
623+
},
624+
})
625+
if err != nil {
626+
return nil, err
627+
}
628+
629+
return pubkeys, nil
630+
}
631+
632+
func (s *Server) storeHistory(email, pubkey string) error {
633+
conn, err := s.dbpool.Take(context.Background())
634+
if err != nil {
635+
return err
636+
}
637+
defer s.dbpool.Put(conn)
638+
639+
return sqlitex.Execute(conn, `
640+
INSERT INTO history (email, pubkey)
641+
VALUES (?, ?)
642+
`, &sqlitex.ExecOptions{
643+
Args: []any{email, pubkey},
644+
})
645+
}
646+
584647
func verifyCaptcha(response string) bool {
585648
if response == "" {
586649
return false

0 commit comments

Comments
 (0)