Skip to content

Commit d031d34

Browse files
committed
cmd/age-keyserver: add transparency log of stored keys
1 parent f191bdb commit d031d34

10 files changed

Lines changed: 314 additions & 11 deletions

File tree

cmd/age-keylookup/main.go

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,45 @@
11
package main
22

33
import (
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

1420
const (
15-
defaultKeyserverURL = "https://keyserver.geomys.org"
21+
defaultKeyserverURL = "https://keyserver.geomys.org"
22+
defaultKeyserverPubkey = "keyserver.geomys.org+16b31509+ARLJ+pmTj78HzTeBj04V+LVfB+GFAQyrg54CRIju7Nn8"
1623
)
1724

1825
func 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+
}

cmd/age-keyserver-keygen/keygen.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package main
2+
3+
import (
4+
"crypto/rand"
5+
"fmt"
6+
"os"
7+
8+
"golang.org/x/mod/sumdb/note"
9+
)
10+
11+
func main() {
12+
if len(os.Args) != 2 {
13+
fmt.Fprintf(os.Stderr, "Usage: %s <origin>\n", os.Args[0])
14+
os.Exit(1)
15+
}
16+
origin := os.Args[1]
17+
18+
skey, vkey, err := note.GenerateKey(rand.Reader, origin)
19+
if err != nil {
20+
fmt.Fprintf(os.Stderr, "Error generating keys: %v\n", err)
21+
os.Exit(1)
22+
}
23+
24+
fmt.Printf("Private key (for LOG_KEY in age-keyserver): %s\n", skey)
25+
fmt.Printf("Public key (for AGE_KEYSERVER_PUBKEY in age-keylookup): %s\n", vkey)
26+
}

0 commit comments

Comments
 (0)