Compare commits
2 Commits
2043df644e
...
7c5d288ca6
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c5d288ca6 | |||
| 553d216a42 |
@@ -22,4 +22,5 @@ WORKDIR /app
|
|||||||
COPY --from=build /out/pgpdashboard /app/pgpdashboard
|
COPY --from=build /out/pgpdashboard /app/pgpdashboard
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
USER nonroot:nonroot
|
USER nonroot:nonroot
|
||||||
|
#KEYSERVER_API_TOKEN='supersecret'
|
||||||
ENTRYPOINT ["/app/pgpdashboard"]
|
ENTRYPOINT ["/app/pgpdashboard"]
|
||||||
|
|||||||
@@ -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",
|
"id": "max_at_send.nrw--D88C7FA9A544ECF8BCCEC6EB8F0B3E5851F2C8CC",
|
||||||
"name": "max",
|
"name": "max",
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
xsFNBGmW+yIBEACmtelaX3reS0veEVzzByS7KhSoaoPVv+WVexNt+s5XNQzUnhUh
|
||||||
|
t0tV6Qh1FPxDFF5oqIs/Ka1R1zwN4Fs6UckW9TV7UoZjuSSyVD1aP8AfCIDl42VU
|
||||||
|
HSVEissRbGlu2D5DKXrmpcSe2Zowe1xov0TrXCiI4E2mtDB1RnRqQhviMQpb0jcv
|
||||||
|
n1L5/3WLc/uRllvGyUxMYmd41tBAwZQpPDU4A0gT5UNNnWGkmhYt/B6eSqCVyLrz
|
||||||
|
rV80oRusA7H7nayOP88FnE+CDAhWDn0K0KY+AOAPqW/37au2vjjjG+9igog+ZoF7
|
||||||
|
V5bJcZ9LUvLer3dkNQOszbJsLqssQK+p7CGBuvMTFaLy4+7tvaaG0/l8LYifTBfJ
|
||||||
|
fkXtHgRSVWJBE28kaMRqp441Tu3azi+GB2XG/qQ8AwlX2O0hdSMhpw9Wh3K9g3Fp
|
||||||
|
Rnk9aO45zqkWD7ZGzWDDuxCRnbT1zFAYJEi94yXqB1nIkDgo+SK7B6ZopXLCDHi+
|
||||||
|
l/3g2ANye58TXnXNgTlFe01/ItOQio6WXjE7vCZeCbYJ3vKTIA8ccEh7OibZMW3g
|
||||||
|
qUQOv9lYu+pmUDqvgvv08oFqySxCcyltR8GKAn9TjEIb67ThnDwSA1d0jFaNTW4h
|
||||||
|
P+6cjEjanD9MwHDy76O2E1zKgqXRs9Kry57ZYV7JwmdEv7w9EAcwtjK1rQARAQAB
|
||||||
|
zR9KYW4gQmVyZ25lciA8amJlcmduZXJAc2VuZC5ucnc+wsGNBBMBCAA3FiEE38Wf
|
||||||
|
7VzkHMenU+nDw312OzijiUwFAmmW+yIFCQWjmoACGwMECwkIBwUVCAkKCwUWAgMB
|
||||||
|
AAAKCRDDfXY7OKOJTCCgD/493NbMnuQ8fRPdfGoIIosrq2K8KuAECulqTDrP6ylf
|
||||||
|
N4IgNNon127sEwGAYoKyX8000kOl9+nrO1MSSQ9BwhR/ZppEw3uKmo2JJkBbZOgI
|
||||||
|
7ifYCESczhL1xZl8shYsohxVfMrBbU1WpAv2LoC0BQtW8HlULLUnqN5oJkVA1ExK
|
||||||
|
v6oa0dQBlcqSGIbOInCpe6P1bljCdTM9aPriu4psH8Vf6nc/I8ZUKGDV1SxbBNXl
|
||||||
|
vgOigb9OdeMmSp9b/LW3EUr7yv9OZku946CibrkZnj6NCEsP1H4swHstgb64gQMR
|
||||||
|
aZmZqdSAos5GI1T1ev1fdqRgjTM9b2SCv2v6CwEvaz+pMD1xuYk9VzXhB1+bG+9S
|
||||||
|
vhDBPzPbezspT1zvGevRGNJd6gsEdHstPeHeMTU7k95oB/j3MXJLlNF5u5OwDiC3
|
||||||
|
eMzLH4gjMob2xePlzjZSAbQHmg0mOWFNWmdiL71DbnUn8FIujUb7RYoPrvFtRy2g
|
||||||
|
m7jrXxWQ7wbwRKkqvLbQkPce3PzIGygeoYgS64oIe1oOVSHahHP+XTpuyI6fLQEQ
|
||||||
|
xWvmK35Z5m6F2VYtoFchYfg+XeAuBnEF5VJ6U+kFHkCEy/gprzd4TuSYqIp51OA7
|
||||||
|
aI461LhOXUX9S8AQq0F/Ua9SqdM7+w8QJtqmjn+4GIqCjUfgsBjsK5Synrq8lYxg
|
||||||
|
0c7BTQRplvsjARAAvFd2rN/9iQUTRUT7NMUddXbKkpAABl1rFReAkYqu0ztaD4JR
|
||||||
|
t0K7JKAMGcsrVpTFhMbeVUyiT06Zvx0wOQNL739SaMCq20oOjFQSl/sm58gBVouv
|
||||||
|
Mv1hKvwaFmgntLphDiOLmCwveULPWUPBfJhKdTFhRv604OazWAW3kb+67giEZq8M
|
||||||
|
oi7YxOeXsYq+uYwMF2mD6MuUJDWNt5p+8ypHRjuq2BFYCA+wAFq1PIzgcvMEpKjp
|
||||||
|
3v6HSGsyqtRT2tBo1AKwvYyoFOM4bKZDHouhF18jbID2R2DRKEOjsp2edBLn2Vnc
|
||||||
|
VuDLUw1JFb9etvyFj5FuFgEdUowwPHvDsdiqqBFq9eGcnJl69xp3XKaCGzUIkG5C
|
||||||
|
mW/0gTgK5kz57uJ5SsXaheM1bNYB9A8YSmAdjywWELM4WPrB7dFNjnFv0hoB/pAG
|
||||||
|
0OtOR18aW5odF96O4Cv0R4X0dcS2MkCwB5pO/UMqYeJaD9m9VTwcL+SJHaGz0y0C
|
||||||
|
KQPo4rBZ2eHsDQcX9FZcrUsVbAUMgN76iObwKiOCcZE7UeVeXJhYB+W3geoUgwZm
|
||||||
|
9QIpkW5b4JgVfx6FwqmYXLX/qhRTf2B9fy+7+sQjHsn6NhDdcQSdnfEqhvowyH3r
|
||||||
|
vEfh+nhhsZq0g3lO4EVHoSIvpNJjarnfdShfHEbUwBtwfLJC7Becvl/22d0AEQEA
|
||||||
|
AcLBfAQYAQgAJhYhBN/Fn+1c5BzHp1Ppw8N9djs4o4lMBQJplvskBQkFo5qAAhsM
|
||||||
|
AAoJEMN9djs4o4lMPEkQAJpMUu0jkewauTFOWzmY2JVQ4JXLQQo2KMMwOxRam5qm
|
||||||
|
76SbJX8iSTF+qSRLNVvVzA8eH20TrGmucyu968argJC4Tb49mW0+j2aaW6oPRomN
|
||||||
|
QQLyMchEcWe/ZrAqRV1QCzZ1RJmKPvU/goBgyb5iXXXguRCmc2Qasj11FgS7mjad
|
||||||
|
jO7jKRsRQPAdlYpjwOxhoLlV8ReyUwXzA9iIzAz7Div8srih5wdlhkt+Kab1mABU
|
||||||
|
TP0hhzGKjKUOhlTTIZ+C30q+1o4uGd0koBAxvacOAd8jN509+OfvteWHoIbNxsEm
|
||||||
|
TRD+w+BbtvNbnq/OrMkBdED0ZP0Ye+o6Vi5B6X3b2kPj2LNI4lun5ZKdxh/NXB0H
|
||||||
|
O0gXcaDmWJnF6c7d/F9ToYwQCdAY9EPx4Fo397QGNr0KMQnkeZhs8rsce8UZ3WPk
|
||||||
|
gbYuRCu0yPFu/1CzGjgTtQOvgMpshvudHHoYIijw1jdBunA7jwfcBdKeZWb277P2
|
||||||
|
tI4SsVb0eJXp7D3HyRue0Z1m57QbtED5CozbYn97NXlnBvHuumqAN//jfezPTyIc
|
||||||
|
dkaSafPYe87Md59B1xYH/9CJFsdkXsGXvT0ziCR6zQdqsiiEeBodEvzBc46x/bm0
|
||||||
|
Ifw23EgUjkRZ0a7phd64yqSlvNnbKTceUD7dR/60vlL88bRr5jbbsrCiuuVf6NZ2
|
||||||
|
=BcaJ
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
55
data/keys/public-2-.asc
Normal file
55
data/keys/public-2-.asc
Normal 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-----
|
||||||
254
main.go
254
main.go
@@ -69,6 +69,35 @@ type store struct {
|
|||||||
byID map[string]KeyRecord
|
byID map[string]KeyRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type apiUploadReq struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Fingerprint string `json:"fingerprint"` // optional, wird sonst aus Key geparsed
|
||||||
|
PublicArmored string `json:"public_armored"` // ASCII armored public key
|
||||||
|
Filename string `json:"filename"` // optional
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiUploadResp struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
WKDHash string `json:"wkd_hash"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Local string `json:"local"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func bearerOK(r *http.Request, token string) bool {
|
||||||
|
if token == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
h := r.Header.Get("Authorization")
|
||||||
|
const p = "Bearer "
|
||||||
|
if !strings.HasPrefix(h, p) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(strings.TrimPrefix(h, p)) == token
|
||||||
|
}
|
||||||
|
|
||||||
func newStore() *store { return &store{byID: make(map[string]KeyRecord)} }
|
func newStore() *store { return &store{byID: make(map[string]KeyRecord)} }
|
||||||
|
|
||||||
// persistSnapshot schreibt eine bereits kopierte Liste auf Disk.
|
// persistSnapshot schreibt eine bereits kopierte Liste auf Disk.
|
||||||
@@ -271,16 +300,38 @@ func zbase32Encode(b []byte) string {
|
|||||||
return string(out)
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// wkdHash: z-base-32(SHA1(strings.ToLower(addr-spec))) + domain
|
// wkdHash: z-base-32(SHA1(strings.ToLower(local-part))) + domain
|
||||||
func wkdHash(email string) (hash string, domain string) {
|
func wkdHash(email string) (hash string, domain string, local string) {
|
||||||
email = strings.ToLower(strings.TrimSpace(email))
|
email = strings.TrimSpace(email)
|
||||||
parts := strings.Split(email, "@")
|
parts := strings.Split(email, "@")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return "", ""
|
return "", "", ""
|
||||||
}
|
}
|
||||||
domain = parts[1]
|
local = parts[0] // keep original for ?l=
|
||||||
s := sha1.Sum([]byte(email))
|
domain = strings.ToLower(parts[1])
|
||||||
return zbase32Encode(s[:]), domain
|
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() {
|
func main() {
|
||||||
@@ -311,6 +362,100 @@ func main() {
|
|||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
apiToken := getenv("KEYSERVER_API_TOKEN", "")
|
||||||
|
mux.HandleFunc("/api/v1/keys", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !bearerOK(r, apiToken) {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit body size
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var req apiUploadReq
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Name = strings.TrimSpace(req.Name)
|
||||||
|
req.Email = strings.TrimSpace(req.Email)
|
||||||
|
req.Fingerprint = strings.TrimSpace(req.Fingerprint)
|
||||||
|
req.PublicArmored = strings.TrimSpace(req.PublicArmored)
|
||||||
|
|
||||||
|
if req.Email == "" || !strings.Contains(req.Email, "@") {
|
||||||
|
http.Error(w, "missing/invalid email", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.PublicArmored == "" || !strings.Contains(req.PublicArmored, "-----BEGIN PGP PUBLIC KEY BLOCK-----") {
|
||||||
|
http.Error(w, "public_armored must be ASCII-armored PGP public key", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b := []byte(req.PublicArmored)
|
||||||
|
|
||||||
|
// Parse fingerprint (authoritative)
|
||||||
|
autoFPR, err := parseFingerprintFromASCII(b)
|
||||||
|
if err != nil || autoFPR == "" {
|
||||||
|
http.Error(w, "could not parse fingerprint from key", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fpr := strings.ToUpper(strings.ReplaceAll(autoFPR, " ", ""))
|
||||||
|
|
||||||
|
// Optional: wenn req.Fingerprint gesetzt ist, validieren wir nur (kein override)
|
||||||
|
if req.Fingerprint != "" && normalizeFPR(req.Fingerprint) != normalizeFPR(fpr) {
|
||||||
|
http.Error(w, "fingerprint mismatch", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// filename
|
||||||
|
fn := strings.TrimSpace(req.Filename)
|
||||||
|
if fn == "" {
|
||||||
|
fn = sanitizeFilename(req.Email)
|
||||||
|
} else {
|
||||||
|
fn = sanitizeFilename(fn)
|
||||||
|
}
|
||||||
|
path := filepath.Join(keysDir, fn)
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, b, 0o644); err != nil {
|
||||||
|
http.Error(w, "save error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := KeyRecord{
|
||||||
|
ID: genID(req.Email, fpr),
|
||||||
|
Name: req.Name,
|
||||||
|
Email: req.Email,
|
||||||
|
Fingerprint: fpr,
|
||||||
|
Filename: fn,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := st.upsert(rec); err != nil {
|
||||||
|
http.Error(w, "index error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WKD meta (wenn du die bereits korrigierte wkdHash(local-part) nutzt)
|
||||||
|
h, d, l := wkdHash(rec.Email) // falls deine Signatur noch (hash,domain) ist
|
||||||
|
// Wenn du die neue wkdHash(email) -> (hash,domain,local) nutzt, dann: h,d,_ := wkdHash(rec.Email)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_ = json.NewEncoder(w).Encode(apiUploadResp{
|
||||||
|
ID: rec.ID,
|
||||||
|
Email: rec.Email,
|
||||||
|
Fingerprint: rec.Fingerprint,
|
||||||
|
WKDHash: h,
|
||||||
|
Domain: d,
|
||||||
|
Local: l,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Live search (HTMX)
|
// Live search (HTMX)
|
||||||
mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
|
||||||
q := r.URL.Query().Get("q")
|
q := r.URL.Query().Get("q")
|
||||||
@@ -322,7 +467,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
// Upload with automatic fingerprint parsing
|
// Upload with automatic fingerprint parsing
|
||||||
mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if enabled("WRITEACCESS", false) {
|
if enabled("WRITEACCESS", true) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
@@ -428,16 +573,26 @@ func main() {
|
|||||||
mux.HandleFunc("/.well-known/openpgpkey/policy", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/.well-known/openpgpkey/policy", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, _ = w.Write([]byte("# WKD policy"))
|
_, _ = w.Write([]byte("# WKD policy\n"))
|
||||||
})
|
})
|
||||||
// direct method: /.well-known/openpgpkey/hu/<hash>
|
// direct method: /.well-known/openpgpkey/hu/<hash>
|
||||||
mux.HandleFunc("/.well-known/openpgpkey/hu/", func(w http.ResponseWriter, r *http.Request) {
|
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 match *KeyRecord
|
||||||
|
var matchLocal string
|
||||||
for _, it := range st.all() {
|
for _, it := range st.all() {
|
||||||
h, _ := wkdHash(it.Email)
|
h, _, local := wkdHash(it.Email)
|
||||||
if h == pathHash {
|
if h == hash {
|
||||||
|
if lParam != "" && lParam != local {
|
||||||
|
continue
|
||||||
|
}
|
||||||
match = &it
|
match = &it
|
||||||
|
matchLocal = local
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,27 +600,70 @@ func main() {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(filepath.Join(keysDir, match.Filename))
|
|
||||||
|
arm, err := os.ReadFile(filepath.Join(keysDir, match.Filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
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.WriteHeader(http.StatusOK)
|
||||||
_, _ = w.Write(data)
|
_, _ = w.Write(bin)
|
||||||
})
|
})
|
||||||
// advanced method: /openpgpkey/<domain>/hu/<hash>
|
// advanced method: /openpgpkey/<domain>/hu/<hash>
|
||||||
mux.HandleFunc("/openpgpkey/", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/.well-known/openpgpkey/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/openpgpkey/"), "/")
|
// 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" {
|
if len(parts) != 3 || parts[1] != "hu" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
domain, hash := parts[0], parts[2]
|
|
||||||
|
domain := strings.ToLower(parts[0])
|
||||||
|
hash := parts[2]
|
||||||
|
lParam := r.URL.Query().Get("l")
|
||||||
|
|
||||||
var match *KeyRecord
|
var match *KeyRecord
|
||||||
for _, it := range st.all() {
|
for _, it := range st.all() {
|
||||||
h, d := wkdHash(it.Email)
|
h, d, local := wkdHash(it.Email)
|
||||||
if h == hash && strings.EqualFold(d, domain) {
|
if h == hash && strings.EqualFold(d, domain) {
|
||||||
|
if lParam != "" && lParam != local {
|
||||||
|
continue
|
||||||
|
}
|
||||||
match = &it
|
match = &it
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -474,14 +672,26 @@ func main() {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(filepath.Join(keysDir, match.Filename))
|
|
||||||
|
arm, err := os.ReadFile(filepath.Join(keysDir, match.Filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
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.WriteHeader(http.StatusOK)
|
||||||
_, _ = w.Write(data)
|
_, _ = w.Write(bin)
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- Minimal HKP-compatible lookup ---
|
// --- Minimal HKP-compatible lookup ---
|
||||||
|
|||||||
Reference in New Issue
Block a user