Update für bessere Kompatibilität

This commit is contained in:
2026-02-19 13:23:54 +01:00
parent 2043df644e
commit 553d216a42
4 changed files with 231 additions and 22 deletions

View File

@@ -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",

View File

@@ -0,0 +1,51 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=BcaJ
-----END PGP PUBLIC KEY BLOCK-----

55
data/keys/public-2-.asc Normal file
View File

@@ -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-----

131
main.go
View File

@@ -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/<hash>
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/<domain>/hu/<hash>
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.<domain>
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/<domain>/hu/<hash>
// oder: /.well-known/openpgpkey/<domain>/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 ---