diff --git a/Dockerfile b/Dockerfile index 39764b0..0cb5fa2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,4 +22,5 @@ WORKDIR /app COPY --from=build /out/pgpdashboard /app/pgpdashboard EXPOSE 8080 USER nonroot:nonroot +#KEYSERVER_API_TOKEN='supersecret' ENTRYPOINT ["/app/pgpdashboard"] diff --git a/main.go b/main.go index 1ae17bd..5e796bc 100644 --- a/main.go +++ b/main.go @@ -69,6 +69,35 @@ type store struct { 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)} } // persistSnapshot schreibt eine bereits kopierte Liste auf Disk. @@ -333,6 +362,100 @@ func main() { 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) mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query().Get("q")