diff --git a/data/index.json b/data/index.json index 7ca1483..ee7c100 100644 --- a/data/index.json +++ b/data/index.json @@ -1,4 +1,20 @@ [ + { + "id": "jbergner_at_send.nrw--DFC59FED5CE41CC7A753E9C3C37D763B38A3894C", + "name": "Jan Bergner", + "email": "jbergner@send.nrw", + "fingerprint": "DFC59FED5CE41CC7A753E9C3C37D763B38A3894C", + "filename": "Jan-Bergner_jbergner@send.nrw-0xC37D763B38A3894C-pub.asc", + "created_at": "2026-02-19T13:00:29.6481446+01:00" + }, + { + "id": "tom.gilbo_at_hilden.de--7160BFBE0D520FDDA0BD3ADA5F09C4BD0EE0BE5A", + "name": "Tom Gilbo", + "email": "tom.gilbo@hilden.de", + "fingerprint": "7160BFBE0D520FDDA0BD3ADA5F09C4BD0EE0BE5A", + "filename": "public-2-.asc", + "created_at": "2026-02-19T12:57:27.4968564+01:00" + }, { "id": "max_at_send.nrw--D88C7FA9A544ECF8BCCEC6EB8F0B3E5851F2C8CC", "name": "max", diff --git a/data/keys/Jan-Bergner_jbergner@send.nrw-0xC37D763B38A3894C-pub.asc b/data/keys/Jan-Bergner_jbergner@send.nrw-0xC37D763B38A3894C-pub.asc new file mode 100644 index 0000000..b875b57 --- /dev/null +++ b/data/keys/Jan-Bergner_jbergner@send.nrw-0xC37D763B38A3894C-pub.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=BcaJ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/data/keys/public-2-.asc b/data/keys/public-2-.asc new file mode 100644 index 0000000..62cf0c2 --- /dev/null +++ b/data/keys/public-2-.asc @@ -0,0 +1,55 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: https://gopenpgp.org +Version: GopenPGP 2.9.0 + +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 +=IwML +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/main.go b/main.go index 7ba1028..1ae17bd 100644 --- a/main.go +++ b/main.go @@ -271,16 +271,38 @@ func zbase32Encode(b []byte) string { return string(out) } -// wkdHash: z-base-32(SHA1(strings.ToLower(addr-spec))) + domain -func wkdHash(email string) (hash string, domain string) { - email = strings.ToLower(strings.TrimSpace(email)) +// wkdHash: z-base-32(SHA1(strings.ToLower(local-part))) + domain +func wkdHash(email string) (hash string, domain string, local string) { + email = strings.TrimSpace(email) parts := strings.Split(email, "@") if len(parts) != 2 { - return "", "" + return "", "", "" } - domain = parts[1] - s := sha1.Sum([]byte(email)) - return zbase32Encode(s[:]), domain + local = parts[0] // keep original for ?l= + domain = strings.ToLower(parts[1]) + lp := strings.ToLower(local) + + s := sha1.Sum([]byte(lp)) + return zbase32Encode(s[:]), domain, local +} + +func armoredToBinary(arm []byte) ([]byte, error) { + ents, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(arm)) + if err != nil { + return nil, err + } + if len(ents) == 0 { + return nil, fmt.Errorf("no keys in armored data") + } + + var out bytes.Buffer + for _, e := range ents { + // Serialize schreibt binary OpenPGP packets (kein Armor) + if err := e.Serialize(&out); err != nil { + return nil, err + } + } + return out.Bytes(), nil } func main() { @@ -322,7 +344,7 @@ func main() { }) // Upload with automatic fingerprint parsing mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) { - if enabled("WRITEACCESS", false) { + if enabled("WRITEACCESS", true) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return @@ -428,16 +450,26 @@ func main() { mux.HandleFunc("/.well-known/openpgpkey/policy", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("# WKD policy")) + _, _ = w.Write([]byte("# WKD policy\n")) }) // direct method: /.well-known/openpgpkey/hu/ mux.HandleFunc("/.well-known/openpgpkey/hu/", func(w http.ResponseWriter, r *http.Request) { - pathHash := strings.TrimPrefix(r.URL.Path, "/.well-known/openpgpkey/hu/") + hash := strings.TrimPrefix(r.URL.Path, "/.well-known/openpgpkey/hu/") + hash = strings.Trim(hash, "/") + + // Optional: ?l=... (unverändert, percent-encoded) — nur als Hint/Check + lParam := r.URL.Query().Get("l") + var match *KeyRecord + var matchLocal string for _, it := range st.all() { - h, _ := wkdHash(it.Email) - if h == pathHash { + h, _, local := wkdHash(it.Email) + if h == hash { + if lParam != "" && lParam != local { + continue + } match = &it + matchLocal = local break } } @@ -445,27 +477,70 @@ func main() { http.NotFound(w, r) return } - data, err := os.ReadFile(filepath.Join(keysDir, match.Filename)) + + arm, err := os.ReadFile(filepath.Join(keysDir, match.Filename)) if err != nil { http.NotFound(w, r) return } - w.Header().Set("Content-Type", "application/pgp-keys") + bin, err := armoredToBinary(arm) + if err != nil { + http.Error(w, "invalid stored key data", http.StatusInternalServerError) + return + } + + // WKD responses are typically application/octet-stream + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Access-Control-Allow-Origin", "*") // optional, hilft manchen Tools + if r.Method == http.MethodHead { + w.WriteHeader(http.StatusOK) + return + } + + _ = matchLocal // falls du später Logging möchtest w.WriteHeader(http.StatusOK) - _, _ = w.Write(data) + _, _ = w.Write(bin) }) // advanced method: /openpgpkey//hu/ - mux.HandleFunc("/openpgpkey/", func(w http.ResponseWriter, r *http.Request) { - parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/openpgpkey/"), "/") + mux.HandleFunc("/.well-known/openpgpkey/", func(w http.ResponseWriter, r *http.Request) { + // Nur advanced bedienen, wenn Host = openpgpkey. + host := r.Host + if i := strings.IndexByte(host, ':'); i >= 0 { + host = host[:i] + } + if !strings.HasPrefix(strings.ToLower(host), "openpgpkey.") { + // Für die Hauptdomain sind policy + /hu/ bereits separat gemappt + http.NotFound(w, r) + return + } + + // Erwartet: + // /.well-known/openpgpkey//hu/ + // oder: /.well-known/openpgpkey//policy + rest := strings.TrimPrefix(r.URL.Path, "/.well-known/openpgpkey/") + parts := strings.Split(strings.Trim(rest, "/"), "/") + if len(parts) == 2 && parts[1] == "policy" { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("# WKD policy\n")) + return + } if len(parts) != 3 || parts[1] != "hu" { http.NotFound(w, r) return } - domain, hash := parts[0], parts[2] + + domain := strings.ToLower(parts[0]) + hash := parts[2] + lParam := r.URL.Query().Get("l") + var match *KeyRecord for _, it := range st.all() { - h, d := wkdHash(it.Email) + h, d, local := wkdHash(it.Email) if h == hash && strings.EqualFold(d, domain) { + if lParam != "" && lParam != local { + continue + } match = &it break } @@ -474,14 +549,26 @@ func main() { http.NotFound(w, r) return } - data, err := os.ReadFile(filepath.Join(keysDir, match.Filename)) + + arm, err := os.ReadFile(filepath.Join(keysDir, match.Filename)) if err != nil { http.NotFound(w, r) return } - w.Header().Set("Content-Type", "application/pgp-keys") + bin, err := armoredToBinary(arm) + if err != nil { + http.Error(w, "invalid stored key data", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == http.MethodHead { + w.WriteHeader(http.StatusOK) + return + } w.WriteHeader(http.StatusOK) - _, _ = w.Write(data) + _, _ = w.Write(bin) }) // --- Minimal HKP-compatible lookup ---