Skip to content

Commit f191bdb

Browse files
FiloSottileclaude
andcommitted
cmd/age-keyserver: boilerplate for centralized email-auth'd keyserver
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 28196bc commit f191bdb

12 files changed

Lines changed: 1591 additions & 0 deletions

File tree

cmd/age-keylookup/main.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"flag"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"os"
11+
"time"
12+
)
13+
14+
const (
15+
defaultKeyserverURL = "https://keyserver.geomys.org"
16+
)
17+
18+
func main() {
19+
flag.Parse()
20+
21+
if flag.NArg() != 1 {
22+
fmt.Fprintf(os.Stderr, "Usage: age-keylookup <email>\n")
23+
fmt.Fprintf(os.Stderr, "\n")
24+
fmt.Fprintf(os.Stderr, "Look up an age public key by email address.\n")
25+
fmt.Fprintf(os.Stderr, "\n")
26+
fmt.Fprintf(os.Stderr, "Example:\n")
27+
fmt.Fprintf(os.Stderr, " age-keylookup filippo@example.com\n")
28+
fmt.Fprintf(os.Stderr, " age -r $(age-keylookup filippo@example.com) -o secret.txt.age secret.txt\n")
29+
fmt.Fprintf(os.Stderr, "\n")
30+
fmt.Fprintf(os.Stderr, "Environment:\n")
31+
fmt.Fprintf(os.Stderr, " AGE_KEYSERVER_URL Default keyserver URL\n")
32+
os.Exit(2)
33+
}
34+
35+
email := flag.Arg(0)
36+
37+
// Determine server URL
38+
server := os.Getenv("AGE_KEYSERVER_URL")
39+
if server == "" {
40+
server = defaultKeyserverURL
41+
}
42+
43+
pubkey, err := lookupKey(server, email)
44+
if err != nil {
45+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
46+
os.Exit(1)
47+
}
48+
49+
fmt.Println(pubkey)
50+
}
51+
52+
func lookupKey(serverURL, email string) (string, error) {
53+
// Build the lookup URL
54+
lookupURL := serverURL + "/api/lookup?email=" + url.QueryEscape(email)
55+
56+
// Create HTTP client with timeout
57+
client := &http.Client{
58+
Timeout: 10 * time.Second,
59+
}
60+
61+
// Make the request
62+
resp, err := client.Get(lookupURL)
63+
if err != nil {
64+
return "", fmt.Errorf("failed to connect to keyserver: %w", err)
65+
}
66+
defer resp.Body.Close()
67+
68+
// Check status code
69+
if resp.StatusCode == http.StatusNotFound {
70+
return "", fmt.Errorf("no key found for %s", email)
71+
}
72+
73+
if resp.StatusCode != http.StatusOK {
74+
body, _ := io.ReadAll(resp.Body)
75+
return "", fmt.Errorf("keyserver error: %s - %s", resp.Status, string(body))
76+
}
77+
78+
// Parse JSON response
79+
var result struct {
80+
Email string `json:"email"`
81+
Pubkey string `json:"pubkey"`
82+
}
83+
84+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
85+
return "", fmt.Errorf("failed to parse response: %w", err)
86+
}
87+
88+
if result.Email != email {
89+
return "", fmt.Errorf("keyserver returned unexpected email: %q", result.Email)
90+
}
91+
if result.Pubkey == "" {
92+
return "", fmt.Errorf("empty public key returned")
93+
}
94+
95+
return result.Pubkey, nil
96+
}

0 commit comments

Comments
 (0)