Skip to content

Commit cfd066e

Browse files
committed
cmd/age-keyserver: hash age public key to prevent log poisoning
1 parent ee2bc49 commit cfd066e

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
@@ -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() {
@@ -144,7 +150,7 @@ func main() {
144150
dbpool, err := sqlitex.NewPool(*dbPath, sqlitex.PoolOptions{
145151
PoolSize: 10,
146152
PrepareConn: func(conn *sqlite.Conn) error {
147-
return sqlitex.ExecuteTransient(conn, schema, nil)
153+
return sqlitex.ExecScript(conn, schema)
148154
},
149155
})
150156
if err != nil {
@@ -367,10 +373,18 @@ func (s *Server) handleSetKey(w http.ResponseWriter, r *http.Request) {
367373

368374
// Compute VRF hash and proof
369375
vrfProof := s.vrf.Prove([]byte(email))
370-
vrfHash := base64.StdEncoding.EncodeToString(vrfProof.Hash())
376+
377+
// Keep track of the unhashed key
378+
if err := s.storeHistory(email, pubkey); err != nil {
379+
http.Error(w, "Failed to store key history", http.StatusInternalServerError)
380+
log.Printf("database error: %v", err)
381+
return
382+
}
371383

372384
// Add to transparency log
373-
entry := tessera.NewEntry(fmt.Appendf(nil, "%s\n%s\n", vrfHash, pubkey))
385+
h := sha256.New()
386+
h.Write([]byte(pubkey))
387+
entry := tessera.NewEntry(h.Sum(vrfProof.Hash())) // vrf-r255(email) || SHA-256(pubkey)
374388
index, _, err := s.awaiter.Await(r.Context(), s.appender.Add(r.Context(), entry))
375389
if err != nil {
376390
http.Error(w, "Failed to add to transparency log", http.StatusInternalServerError)
@@ -454,11 +468,19 @@ func (s *Server) handleMonitor(w http.ResponseWriter, r *http.Request) {
454468
return
455469
}
456470

471+
history, err := s.getHistory(email)
472+
if err != nil {
473+
http.Error(w, "Database error", http.StatusInternalServerError)
474+
log.Printf("database error: %v", err)
475+
return
476+
}
477+
457478
// Return as JSON
458479
w.Header().Set("Content-Type", "application/json")
459480
json.NewEncoder(w).Encode(map[string]any{
460481
"email": email,
461482
"vrf_proof": s.vrf.Prove([]byte(email)).Bytes(),
483+
"history": history,
462484
})
463485
}
464486

@@ -594,6 +616,47 @@ func (s *Server) deleteKey(email string) error {
594616
})
595617
}
596618

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

0 commit comments

Comments
 (0)