Skip to content

Commit 194a603

Browse files
committed
cmd/age-keyserver: hash age public key to prevent log poisoning
1 parent 52f07f1 commit 194a603

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"
@@ -164,8 +166,9 @@ func lookupKey(serverURL string, policy torchwood.Policy, vrfKey *vrf.PublicKey,
164166
}
165167

166168
// Verify spicy signature
167-
vrfHashB64 := base64.StdEncoding.EncodeToString(vrfHash)
168-
entry := fmt.Appendf(nil, "%s\n%s\n", vrfHashB64, result.Pubkey)
169+
h := sha256.New()
170+
h.Write([]byte(result.Pubkey))
171+
entry := h.Sum(vrfHash) // vrf-r255(email) || SHA-256(pubkey)
169172
if err := torchwood.VerifyProof(policy, tlog.RecordHash(entry), []byte(result.Proof)); err != nil {
170173
return "", fmt.Errorf("failed to verify key proof: %w", err)
171174
}
@@ -174,7 +177,7 @@ func lookupKey(serverURL string, policy torchwood.Policy, vrfKey *vrf.PublicKey,
174177
}
175178

176179
func monitorLog(serverURL string, policy torchwood.Policy, vrfKey *vrf.PublicKey, email string) ([]string, error) {
177-
// Request the VRF proof from the monitor endpoint
180+
// Request the VRF proof and history from the monitor endpoint
178181
monitorURL := serverURL + "/api/monitor?email=" + url.QueryEscape(email)
179182
client := &http.Client{
180183
Timeout: 10 * time.Second,
@@ -192,8 +195,9 @@ func monitorLog(serverURL string, policy torchwood.Policy, vrfKey *vrf.PublicKey
192195
return nil, fmt.Errorf("keyserver error: %s - %s", resp.Status, string(body))
193196
}
194197
var result struct {
195-
Email string `json:"email"`
196-
VRFProof []byte `json:"vrf_proof"`
198+
Email string `json:"email"`
199+
VRFProof []byte `json:"vrf_proof"`
200+
History []string `json:"history"`
197201
}
198202
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
199203
return nil, fmt.Errorf("failed to parse response: %w", err)
@@ -202,6 +206,13 @@ func monitorLog(serverURL string, policy torchwood.Policy, vrfKey *vrf.PublicKey
202206
return nil, fmt.Errorf("keyserver returned unexpected email: %q", result.Email)
203207
}
204208

209+
// Prepare map of hashes of historical keys
210+
historyHashes := make(map[[32]byte]string)
211+
for _, pk := range result.History {
212+
h := sha256.Sum256([]byte(pk))
213+
historyHashes[h] = pk
214+
}
215+
205216
// Compute and verify VRF hash
206217
vrfProof, err := vrf.NewProof(result.VRFProof)
207218
if err != nil {
@@ -234,17 +245,17 @@ func monitorLog(serverURL string, policy torchwood.Policy, vrfKey *vrf.PublicKey
234245
// Fetch all entries up to the checkpoint size
235246
var pubkeys []string
236247
for i, entry := range c.AllEntries(context.Background(), checkpoint.Tree, 0) {
237-
e, rest, ok := strings.Cut(string(entry), "\n")
238-
if !ok {
239-
return nil, fmt.Errorf("malformed log entry %d: %q", i, string(entry))
248+
if len(entry) != 64+32 {
249+
return nil, fmt.Errorf("invalid entry size at index %d", i)
240250
}
241-
k, rest, ok := strings.Cut(rest, "\n")
242-
if !ok || rest != "" {
243-
return nil, fmt.Errorf("malformed log entry %d: %q", i, string(entry))
251+
if !bytes.Equal(entry[:64], vrfHash) {
252+
continue
244253
}
245-
if e == base64.StdEncoding.EncodeToString(vrfHash) {
246-
pubkeys = append(pubkeys, k)
254+
pk, ok := historyHashes[([32]byte)(entry[64:])]
255+
if !ok {
256+
return nil, fmt.Errorf("found unknown public key hash in log at index %d", i)
247257
}
258+
pubkeys = append(pubkeys, pk)
248259
}
249260
if c.Err() != nil {
250261
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
@@ -72,7 +72,13 @@ const (
7272
CREATE TABLE IF NOT EXISTS keys (
7373
email TEXT PRIMARY KEY,
7474
json_data BLOB
75-
) STRICT;`
75+
) STRICT;
76+
CREATE TABLE IF NOT EXISTS history (
77+
email TEXT NOT NULL,
78+
pubkey TEXT NOT NULL
79+
) STRICT;
80+
CREATE INDEX IF NOT EXISTS history_email_idx ON history(email);
81+
`
7682
)
7783

7884
func main() {
@@ -146,7 +152,7 @@ func main() {
146152
dbpool, err := sqlitex.NewPool(*dbPath, sqlitex.PoolOptions{
147153
PoolSize: 10,
148154
PrepareConn: func(conn *sqlite.Conn) error {
149-
return sqlitex.ExecuteTransient(conn, schema, nil)
155+
return sqlitex.ExecScript(conn, schema)
150156
},
151157
})
152158
if err != nil {
@@ -376,10 +382,18 @@ func (s *Server) handleSetKey(w http.ResponseWriter, r *http.Request) {
376382

377383
// Compute VRF hash and proof
378384
vrfProof := s.vrf.Prove([]byte(email))
379-
vrfHash := base64.StdEncoding.EncodeToString(vrfProof.Hash())
385+
386+
// Keep track of the unhashed key
387+
if err := s.storeHistory(email, pubkey); err != nil {
388+
http.Error(w, "Failed to store key history", http.StatusInternalServerError)
389+
log.Printf("database error: %v", err)
390+
return
391+
}
380392

381393
// Add to transparency log
382-
entry := tessera.NewEntry(fmt.Appendf(nil, "%s\n%s\n", vrfHash, pubkey))
394+
h := sha256.New()
395+
h.Write([]byte(pubkey))
396+
entry := tessera.NewEntry(h.Sum(vrfProof.Hash())) // vrf-r255(email) || SHA-256(pubkey)
383397
index, _, err := s.awaiter.Await(r.Context(), s.appender.Add(r.Context(), entry))
384398
if err != nil {
385399
http.Error(w, "Failed to add to transparency log", http.StatusInternalServerError)
@@ -463,11 +477,19 @@ func (s *Server) handleMonitor(w http.ResponseWriter, r *http.Request) {
463477
return
464478
}
465479

480+
history, err := s.getHistory(email)
481+
if err != nil {
482+
http.Error(w, "Database error", http.StatusInternalServerError)
483+
log.Printf("database error: %v", err)
484+
return
485+
}
486+
466487
// Return as JSON
467488
w.Header().Set("Content-Type", "application/json")
468489
json.NewEncoder(w).Encode(map[string]any{
469490
"email": email,
470491
"vrf_proof": s.vrf.Prove([]byte(email)).Bytes(),
492+
"history": history,
471493
})
472494
}
473495

@@ -599,6 +621,47 @@ func (s *Server) deleteKey(email string) error {
599621
})
600622
}
601623

624+
func (s *Server) getHistory(email string) ([]string, error) {
625+
conn, err := s.dbpool.Take(context.Background())
626+
if err != nil {
627+
return nil, err
628+
}
629+
defer s.dbpool.Put(conn)
630+
631+
var pubkeys []string
632+
err = sqlitex.Execute(conn, `
633+
SELECT pubkey FROM history
634+
WHERE email = ?
635+
`, &sqlitex.ExecOptions{
636+
Args: []any{email},
637+
ResultFunc: func(stmt *sqlite.Stmt) error {
638+
pubkey := stmt.ColumnText(0)
639+
pubkeys = append(pubkeys, pubkey)
640+
return nil
641+
},
642+
})
643+
if err != nil {
644+
return nil, err
645+
}
646+
647+
return pubkeys, nil
648+
}
649+
650+
func (s *Server) storeHistory(email, pubkey string) error {
651+
conn, err := s.dbpool.Take(context.Background())
652+
if err != nil {
653+
return err
654+
}
655+
defer s.dbpool.Put(conn)
656+
657+
return sqlitex.Execute(conn, `
658+
INSERT INTO history (email, pubkey)
659+
VALUES (?, ?)
660+
`, &sqlitex.ExecOptions{
661+
Args: []any{email, pubkey},
662+
})
663+
}
664+
602665
func verifyCaptcha(response string) bool {
603666
if response == "" {
604667
return false

0 commit comments

Comments
 (0)