Skip to content

Commit a476a04

Browse files
committed
cmd/age-keyserver: use VRFs to hide emails in the log
1 parent 2985431 commit a476a04

7 files changed

Lines changed: 127 additions & 20 deletions

File tree

cmd/age-keylookup/main.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"encoding/base64"
45
"encoding/json"
56
"fmt"
67
"io"
@@ -10,6 +11,7 @@ import (
1011
"strings"
1112
"time"
1213

14+
"filippo.io/mostly-harmless/vrf-r255"
1315
"filippo.io/torchwood"
1416
"golang.org/x/mod/sumdb/note"
1517
"golang.org/x/mod/sumdb/tlog"
@@ -28,6 +30,7 @@ func main() {
2830
fmt.Fprintf(os.Stderr, "Environment:\n")
2931
fmt.Fprintf(os.Stderr, " AGE_KEYSERVER_URL Default keyserver URL\n")
3032
fmt.Fprintf(os.Stderr, " AGE_KEYSERVER_PUBKEY Default keyserver transparency log vkey\n")
33+
fmt.Fprintf(os.Stderr, " AGE_KEYSERVER_VRFKEY Default keyserver transparency log VRF public key\n")
3134
os.Exit(2)
3235
}
3336

@@ -49,7 +52,22 @@ func main() {
4952
os.Exit(1)
5053
}
5154

52-
pubkey, err := lookupKey(server, v, email)
55+
vrfKeyB64 := os.Getenv("AGE_KEYSERVER_VRFKEY")
56+
if vrfKeyB64 == "" {
57+
vrfKeyB64 = "vKHX1vKXl7yF0qBiDxCUXWgOHlapMvqFeIBXt7c29iQ="
58+
}
59+
vrfKeyBytes, err := base64.StdEncoding.DecodeString(vrfKeyB64)
60+
if err != nil {
61+
fmt.Fprintf(os.Stderr, "Error: invalid base64 keyserver VRF public key: %v\n", err)
62+
os.Exit(1)
63+
}
64+
vrfKey, err := vrf.NewPublicKey(vrfKeyBytes)
65+
if err != nil {
66+
fmt.Fprintf(os.Stderr, "Error: invalid keyserver VRF public key: %v\n", err)
67+
os.Exit(1)
68+
}
69+
70+
pubkey, err := lookupKey(server, v, vrfKey, email)
5371
if err != nil {
5472
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
5573
os.Exit(1)
@@ -58,7 +76,7 @@ func main() {
5876
fmt.Println(pubkey)
5977
}
6078

61-
func lookupKey(serverURL string, v note.Verifier, email string) (string, error) {
79+
func lookupKey(serverURL string, v note.Verifier, vrfKey *vrf.PublicKey, email string) (string, error) {
6280
// Normalize email
6381
email = strings.TrimSpace(strings.ToLower(email))
6482

@@ -105,8 +123,23 @@ func lookupKey(serverURL string, v note.Verifier, email string) (string, error)
105123
return "", fmt.Errorf("empty public key returned")
106124
}
107125

126+
// Compute and verify VRF hash
127+
vrfProofBytes, err := torchwood.HintFromProof([]byte(result.Proof))
128+
if err != nil {
129+
return "", fmt.Errorf("failed to extract VRF proof: %w", err)
130+
}
131+
vrfProof, err := vrf.NewProof(vrfProofBytes)
132+
if err != nil {
133+
return "", fmt.Errorf("failed to parse VRF proof: %w", err)
134+
}
135+
vrfHash, err := vrfKey.Verify(vrfProof, []byte(email))
136+
if err != nil {
137+
return "", fmt.Errorf("failed to verify VRF proof: %w", err)
138+
}
139+
108140
// Verify spicy signature
109-
entry := fmt.Appendf(nil, "%s\n%s\n", result.Email, result.Pubkey)
141+
vrfHashB64 := base64.StdEncoding.EncodeToString(vrfHash)
142+
entry := fmt.Appendf(nil, "%s\n%s\n", vrfHashB64, result.Pubkey)
110143
if err := torchwood.VerifyProof(v.Name(), func(b []byte) (*note.Note, error) {
111144
return note.Open(b, note.VerifierList(v))
112145
}, tlog.RecordHash(entry), []byte(result.Proof)); err != nil {

cmd/age-keylookup/main_test.go

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"crypto/rand"
6+
"encoding/base64"
67
"encoding/json"
78
"fmt"
89
"html/template"
@@ -16,6 +17,7 @@ import (
1617
"time"
1718

1819
"filippo.io/age"
20+
"filippo.io/mostly-harmless/vrf-r255"
1921
"filippo.io/torchwood"
2022
"filippo.io/torchwood/tesserax"
2123
"github.com/transparency-dev/tessera"
@@ -33,6 +35,7 @@ type Server struct {
3335
dbpool *sqlitex.Pool
3436
templates *template.Template
3537
hmacKey []byte
38+
vrf *vrf.PrivateKey
3639
reader tessera.LogReader
3740
appender *tessera.Appender
3841
awaiter *tessera.PublicationAwaiter
@@ -43,6 +46,7 @@ type KeyData struct {
4346
Pubkey string `json:"pubkey"`
4447
UpdatedAt int64 `json:"updated_at"`
4548
LogIndex int64 `json:"log_index"`
49+
VRFProof []byte `json:"vrf_proof"`
4650
}
4751

4852
const schema = `CREATE TABLE IF NOT EXISTS keys (email TEXT PRIMARY KEY, json_data BLOB) STRICT;`
@@ -101,13 +105,17 @@ func TestCLIDirect(t *testing.T) {
101105
defer shutdown(context.Background())
102106
awaiter := tessera.NewPublicationAwaiter(ctx, logReader.ReadCheckpoint, 250*time.Millisecond)
103107

108+
// Generate test VRF key
109+
vrfKey := vrf.GenerateKey()
110+
104111
// Create minimal templates
105112
tmpl := template.Must(template.New("test").Parse(""))
106113

107114
srv := &Server{
108115
dbpool: dbpool,
109116
templates: tmpl,
110117
hmacKey: hmacKey,
118+
vrf: vrfKey,
111119
reader: logReader,
112120
appender: appender,
113121
awaiter: awaiter,
@@ -140,8 +148,12 @@ func TestCLIDirect(t *testing.T) {
140148
testKey := identity.Recipient().String()
141149
testEmail := "test@example.com"
142150

151+
// Compute VRF hash and proof
152+
vrfProof := srv.vrf.Prove([]byte(testEmail))
153+
vrfHash := base64.StdEncoding.EncodeToString(vrfProof.Hash())
154+
143155
// Add to transparency log
144-
entry := tessera.NewEntry(fmt.Appendf(nil, "%s\n%s\n", testEmail, testKey))
156+
entry := tessera.NewEntry(fmt.Appendf(nil, "%s\n%s\n", vrfHash, testKey))
145157
index, _, err := srv.awaiter.Await(ctx, srv.appender.Add(ctx, entry))
146158
if err != nil {
147159
t.Fatal(err)
@@ -152,6 +164,7 @@ func TestCLIDirect(t *testing.T) {
152164
Pubkey: testKey,
153165
UpdatedAt: time.Now().Unix(),
154166
LogIndex: int64(index.Index),
167+
VRFProof: vrfProof.Bytes(),
155168
}
156169
jsonData, _ := json.Marshal(data)
157170

@@ -171,7 +184,7 @@ func TestCLIDirect(t *testing.T) {
171184
}
172185

173186
// Test the lookup function
174-
pubkey, err := lookupKey(serverURL, verifier, testEmail)
187+
pubkey, err := lookupKey(serverURL, verifier, srv.vrf.PublicKey(), testEmail)
175188
if err != nil {
176189
t.Fatalf("lookup failed: %v", err)
177190
}
@@ -182,7 +195,7 @@ func TestCLIDirect(t *testing.T) {
182195
})
183196

184197
t.Run("lookup non-existent key", func(t *testing.T) {
185-
_, err := lookupKey(serverURL, verifier, "nonexistent@example.com")
198+
_, err := lookupKey(serverURL, verifier, srv.vrf.PublicKey(), "nonexistent@example.com")
186199
if err == nil {
187200
t.Error("expected error for non-existent key")
188201
}
@@ -204,8 +217,12 @@ func TestCLIDirect(t *testing.T) {
204217
testKey := identity.Recipient().String()
205218
expectedKeys[email] = testKey
206219

220+
// Compute VRF hash and proof
221+
vrfProof := srv.vrf.Prove([]byte(email))
222+
vrfHash := base64.StdEncoding.EncodeToString(vrfProof.Hash())
223+
207224
// Add to transparency log
208-
entry := tessera.NewEntry(fmt.Appendf(nil, "%s\n%s\n", email, testKey))
225+
entry := tessera.NewEntry(fmt.Appendf(nil, "%s\n%s\n", vrfHash, testKey))
209226
index, _, err := srv.awaiter.Await(ctx, srv.appender.Add(ctx, entry))
210227
if err != nil {
211228
t.Fatal(err)
@@ -216,6 +233,7 @@ func TestCLIDirect(t *testing.T) {
216233
Pubkey: testKey,
217234
UpdatedAt: time.Now().Unix(),
218235
LogIndex: int64(index.Index),
236+
VRFProof: vrfProof.Bytes(),
219237
}
220238
jsonData, _ := json.Marshal(data)
221239

@@ -237,7 +255,7 @@ func TestCLIDirect(t *testing.T) {
237255

238256
// Test lookup for each key
239257
for _, email := range emails {
240-
pubkey, err := lookupKey(serverURL, verifier, email)
258+
pubkey, err := lookupKey(serverURL, verifier, srv.vrf.PublicKey(), email)
241259
if err != nil {
242260
t.Fatalf("lookup failed for %s: %v", email, err)
243261
}
@@ -264,7 +282,7 @@ func (s *Server) handleLookup(w http.ResponseWriter, r *http.Request) {
264282
}
265283

266284
// Generate proof
267-
proof, err := s.makeSpicySignature(r.Context(), email, data.Pubkey, data.LogIndex)
285+
proof, err := s.makeSpicySignature(r.Context(), data.LogIndex, data.VRFProof)
268286
if err != nil {
269287
http.Error(w, "Failed to create proof", http.StatusInternalServerError)
270288
log.Printf("proof error: %v", err)
@@ -315,7 +333,7 @@ func (s *Server) getKeyData(email string) *KeyData {
315333
return &data
316334
}
317335

318-
func (s *Server) makeSpicySignature(ctx context.Context, email, pubkey string, index int64) ([]byte, error) {
336+
func (s *Server) makeSpicySignature(ctx context.Context, index int64, vrfProof []byte) ([]byte, error) {
319337
checkpoint, err := s.reader.ReadCheckpoint(ctx)
320338
if err != nil {
321339
return nil, fmt.Errorf("failed to read checkpoint: %v", err)
@@ -333,7 +351,7 @@ func (s *Server) makeSpicySignature(ctx context.Context, email, pubkey string, i
333351
if err != nil {
334352
return nil, fmt.Errorf("failed to create proof: %v", err)
335353
}
336-
return torchwood.FormatProof(index, p, checkpoint), nil
354+
return torchwood.FormatProofWithHint(index, vrfProof, p, checkpoint), nil
337355
}
338356

339357
// scripttest integration tests
@@ -389,6 +407,7 @@ type testEnv struct {
389407
serverURL string
390408
dbPath string
391409
verifier note.Verifier
410+
vrfPubKey *vrf.PublicKey
392411
server *Server
393412
cleanup func()
394413
}
@@ -462,13 +481,17 @@ func startServerCmd() script.Cmd {
462481
}
463482
awaiter := tessera.NewPublicationAwaiter(ctx, logReader.ReadCheckpoint, 250*time.Millisecond)
464483

484+
// Generate test VRF key
485+
vrfKey := vrf.GenerateKey()
486+
465487
// Create minimal templates
466488
tmpl := template.Must(template.New("test").Parse(""))
467489

468490
srv := &Server{
469491
dbpool: dbpool,
470492
templates: tmpl,
471493
hmacKey: hmacKey,
494+
vrf: vrfKey,
472495
reader: logReader,
473496
appender: appender,
474497
awaiter: awaiter,
@@ -495,6 +518,7 @@ func startServerCmd() script.Cmd {
495518
globalEnv.serverURL = serverURL
496519
globalEnv.dbPath = dbPath
497520
globalEnv.verifier = verifier
521+
globalEnv.vrfPubKey = vrfKey.PublicKey()
498522
globalEnv.server = srv
499523
globalEnv.cleanup = func() {
500524
testServer.Close()
@@ -536,8 +560,12 @@ func insertKeyCmd() script.Cmd {
536560

537561
ctx := context.Background()
538562

563+
// Compute VRF hash and proof
564+
vrfProof := globalEnv.server.vrf.Prove([]byte(email))
565+
vrfHash := base64.StdEncoding.EncodeToString(vrfProof.Hash())
566+
539567
// Add to transparency log
540-
entry := tessera.NewEntry(fmt.Appendf(nil, "%s\n%s\n", email, pubkey))
568+
entry := tessera.NewEntry(fmt.Appendf(nil, "%s\n%s\n", vrfHash, pubkey))
541569
index, _, err := globalEnv.server.awaiter.Await(ctx, globalEnv.server.appender.Add(ctx, entry))
542570
if err != nil {
543571
return nil, fmt.Errorf("failed to add to transparency log: %w", err)
@@ -548,6 +576,7 @@ func insertKeyCmd() script.Cmd {
548576
Pubkey: pubkey,
549577
UpdatedAt: time.Now().Unix(),
550578
LogIndex: int64(index.Index),
579+
VRFProof: vrfProof.Bytes(),
551580
}
552581

553582
jsonData, err := json.Marshal(data)
@@ -600,9 +629,12 @@ func cliCmd() script.Cmd {
600629
if globalEnv.verifier == nil {
601630
return nil, fmt.Errorf("no verifier available (use startserver first)")
602631
}
632+
if globalEnv.vrfPubKey == nil {
633+
return nil, fmt.Errorf("no VRF public key available (use startserver first)")
634+
}
603635

604636
// Call the lookup function directly
605-
pubkey, err := lookupKey(serverURL, globalEnv.verifier, email)
637+
pubkey, err := lookupKey(serverURL, globalEnv.verifier, globalEnv.vrfPubKey, email)
606638
if err != nil {
607639
return nil, err
608640
}
@@ -632,6 +664,7 @@ func stopServerCmd() script.Cmd {
632664
globalEnv.serverURL = ""
633665
globalEnv.dbPath = ""
634666
globalEnv.verifier = nil
667+
globalEnv.vrfPubKey = nil
635668
globalEnv.server = nil
636669
}
637670

cmd/age-keyserver-keygen/keygen.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package main
22

33
import (
44
"crypto/rand"
5+
"encoding/base64"
56
"fmt"
67
"os"
78

9+
"filippo.io/mostly-harmless/vrf-r255"
810
"golang.org/x/mod/sumdb/note"
911
)
1012

@@ -21,6 +23,10 @@ func main() {
2123
os.Exit(1)
2224
}
2325

26+
vrfKey := vrf.GenerateKey()
27+
2428
fmt.Printf("Private key (for LOG_KEY in keyserver): %s\n", skey)
29+
fmt.Printf("Private VRF key (for VRF_KEY in keyserver): %s\n", base64.StdEncoding.EncodeToString(vrfKey.Bytes()))
2530
fmt.Printf("Public key (for AGE_KEYSERVER_PUBKEY in age-keylookup): %s\n", vkey)
31+
fmt.Printf("Public VRF key (for AGE_KEYSERVER_VRFKEY in age-keylookup): %s\n", base64.StdEncoding.EncodeToString(vrfKey.PublicKey().Bytes()))
2632
}

0 commit comments

Comments
 (0)