11package main
22
33import (
4+ "context"
45 "encoding/json"
56 "flag"
67 "fmt"
78 "io"
89 "net/http"
910 "net/url"
1011 "os"
12+ "strings"
1113 "time"
14+
15+ "filippo.io/torchwood"
16+ "golang.org/x/mod/sumdb/note"
17+ "golang.org/x/mod/sumdb/tlog"
1218)
1319
1420const (
15- defaultKeyserverURL = "https://keyserver.geomys.org"
21+ defaultKeyserverURL = "https://keyserver.geomys.org"
22+ defaultKeyserverPubkey = "keyserver.geomys.org+16b31509+ARLJ+pmTj78HzTeBj04V+LVfB+GFAQyrg54CRIju7Nn8"
1623)
1724
1825func main () {
26+ allFlag := flag .Bool ("all" , false , "list all public keys in the transparency log" )
1927 flag .Parse ()
2028
2129 if flag .NArg () != 1 {
22- fmt .Fprintf (os .Stderr , "Usage: age-keylookup <email>\n " )
30+ fmt .Fprintf (os .Stderr , "Usage: age-keylookup [-all] <email>\n " )
2331 fmt .Fprintf (os .Stderr , "\n " )
2432 fmt .Fprintf (os .Stderr , "Look up an age public key by email address.\n " )
2533 fmt .Fprintf (os .Stderr , "\n " )
34+ fmt .Fprintf (os .Stderr , "With -all, it enumerates all public keys in the transparency log.\n " )
35+ fmt .Fprintf (os .Stderr , "\n " )
2636 fmt .Fprintf (os .Stderr , "Example:\n " )
2737 fmt .Fprintf (os .Stderr , " age-keylookup filippo@example.com\n " )
2838 fmt .Fprintf (os .Stderr , " age -r $(age-keylookup filippo@example.com) -o secret.txt.age secret.txt\n " )
2939 fmt .Fprintf (os .Stderr , "\n " )
3040 fmt .Fprintf (os .Stderr , "Environment:\n " )
3141 fmt .Fprintf (os .Stderr , " AGE_KEYSERVER_URL Default keyserver URL\n " )
42+ fmt .Fprintf (os .Stderr , " AGE_KEYSERVER_PUBKEY Default keyserver transparency log vkey\n " )
3243 os .Exit (2 )
3344 }
3445
@@ -40,7 +51,33 @@ func main() {
4051 server = defaultKeyserverURL
4152 }
4253
43- pubkey , err := lookupKey (server , email )
54+ vkey := os .Getenv ("AGE_KEYSERVER_PUBKEY" )
55+ if vkey == "" {
56+ vkey = defaultKeyserverPubkey
57+ }
58+ v , err := note .NewVerifier (vkey )
59+ if err != nil {
60+ fmt .Fprintf (os .Stderr , "Error: invalid keyserver public key: %v\n " , err )
61+ os .Exit (1 )
62+ }
63+ policy := torchwood .ThresholdPolicy (2 , torchwood .OriginPolicy (v .Name ()), torchwood .SingleVerifierPolicy (v ))
64+
65+ // Normalize email
66+ email = strings .TrimSpace (strings .ToLower (email ))
67+
68+ if * allFlag {
69+ pubkeys , err := monitorLog (server , policy , email )
70+ if err != nil {
71+ fmt .Fprintf (os .Stderr , "Error: %v\n " , err )
72+ os .Exit (1 )
73+ }
74+ for _ , pk := range pubkeys {
75+ fmt .Println (pk )
76+ }
77+ return
78+ }
79+
80+ pubkey , err := lookupKey (server , policy , email )
4481 if err != nil {
4582 fmt .Fprintf (os .Stderr , "Error: %v\n " , err )
4683 os .Exit (1 )
@@ -49,7 +86,7 @@ func main() {
4986 fmt .Println (pubkey )
5087}
5188
52- func lookupKey (serverURL , email string ) (string , error ) {
89+ func lookupKey (serverURL string , policy torchwood. Policy , email string ) (string , error ) {
5390 // Build the lookup URL
5491 lookupURL := serverURL + "/api/lookup?email=" + url .QueryEscape (email )
5592
@@ -79,6 +116,7 @@ func lookupKey(serverURL, email string) (string, error) {
79116 var result struct {
80117 Email string `json:"email"`
81118 Pubkey string `json:"pubkey"`
119+ Proof string `json:"proof"`
82120 }
83121
84122 if err := json .NewDecoder (resp .Body ).Decode (& result ); err != nil {
@@ -92,5 +130,53 @@ func lookupKey(serverURL, email string) (string, error) {
92130 return "" , fmt .Errorf ("empty public key returned" )
93131 }
94132
133+ // Verify spicy signature
134+ entry := fmt .Appendf (nil , "%s\n %s\n " , result .Email , result .Pubkey )
135+ if err := torchwood .VerifyProof (policy , tlog .RecordHash (entry ), []byte (result .Proof )); err != nil {
136+ return "" , fmt .Errorf ("failed to verify key proof: %w" , err )
137+ }
138+
95139 return result .Pubkey , nil
96140}
141+
142+ func monitorLog (serverURL string , policy torchwood.Policy , email string ) ([]string , error ) {
143+ f , err := torchwood .NewTileFetcher (serverURL + "/tlog" , torchwood .WithUserAgent ("age-keylookup/1.0" ))
144+ if err != nil {
145+ return nil , fmt .Errorf ("failed to create tile fetcher: %w" , err )
146+ }
147+ c , err := torchwood .NewClient (f )
148+ if err != nil {
149+ return nil , fmt .Errorf ("failed to create torchwood client: %w" , err )
150+ }
151+
152+ // Fetch and verify checkpoint
153+ signedCheckpoint , err := f .ReadEndpoint (context .Background (), "checkpoint" )
154+ if err != nil {
155+ return nil , fmt .Errorf ("failed to read checkpoint: %w" , err )
156+ }
157+ checkpoint , _ , err := torchwood .VerifyCheckpoint (signedCheckpoint , policy )
158+ if err != nil {
159+ return nil , fmt .Errorf ("failed to parse checkpoint: %w" , err )
160+ }
161+
162+ // Fetch all entries up to the checkpoint size
163+ var pubkeys []string
164+ for i , entry := range c .AllEntries (context .Background (), checkpoint .Tree , 0 ) {
165+ e , rest , ok := strings .Cut (string (entry ), "\n " )
166+ if ! ok {
167+ return nil , fmt .Errorf ("malformed log entry %d: %q" , i , string (entry ))
168+ }
169+ k , rest , ok := strings .Cut (rest , "\n " )
170+ if ! ok || rest != "" {
171+ return nil , fmt .Errorf ("malformed log entry %d: %q" , i , string (entry ))
172+ }
173+ if e == email {
174+ pubkeys = append (pubkeys , k )
175+ }
176+ }
177+ if c .Err () != nil {
178+ return nil , fmt .Errorf ("error fetching log entries: %w" , c .Err ())
179+ }
180+
181+ return pubkeys , nil
182+ }
0 commit comments