136 lines
3.7 KiB
Go
136 lines
3.7 KiB
Go
// Package httpapi exposes the signing-service's single endpoint:
|
|
// POST /api/v1/sign/dsse per ADR-0005.
|
|
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"basicstack.de/proofedmesh/services/signing-service/internal/policy"
|
|
"basicstack.de/proofedmesh/services/signing-service/internal/signer"
|
|
"basicstack.de/proofedmesh/services/signing-service/internal/token"
|
|
)
|
|
|
|
type Server struct {
|
|
Signer signer.Signer
|
|
Policy policy.Decider
|
|
Validator token.Validator
|
|
Logger *slog.Logger
|
|
Now func() time.Time
|
|
}
|
|
|
|
func (s Server) Mount(mux *http.ServeMux) {
|
|
mux.HandleFunc("POST /api/v1/sign/dsse", s.handleSign)
|
|
mux.HandleFunc("GET /healthz", s.handleHealth)
|
|
}
|
|
|
|
type signRequest struct {
|
|
KeyRef string `json:"key_ref"`
|
|
Statement signer.Statement `json:"statement"`
|
|
}
|
|
|
|
type signResponse struct {
|
|
Envelope signer.Envelope `json:"envelope"`
|
|
ContentHash string `json:"content_hash"`
|
|
SignedAt time.Time `json:"signed_at"`
|
|
}
|
|
|
|
type errResponse struct {
|
|
Error errBody `json:"error"`
|
|
}
|
|
|
|
type errBody struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func (s Server) handleHealth(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
|
}
|
|
|
|
func (s Server) handleSign(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
bearer, err := token.FromAuthHeader(r)
|
|
if err != nil {
|
|
writeErr(w, http.StatusUnauthorized, "unauthorized", "missing or malformed Authorization header")
|
|
return
|
|
}
|
|
subject, err := s.Validator.Validate(ctx, bearer)
|
|
if err != nil {
|
|
writeErr(w, http.StatusUnauthorized, "unauthorized", err.Error())
|
|
return
|
|
}
|
|
|
|
var req signRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeErr(w, http.StatusBadRequest, "bad_request", fmt.Sprintf("decode body: %v", err))
|
|
return
|
|
}
|
|
if req.KeyRef == "" {
|
|
writeErr(w, http.StatusBadRequest, "bad_request", "key_ref is required")
|
|
return
|
|
}
|
|
if req.Statement == nil {
|
|
writeErr(w, http.StatusBadRequest, "bad_request", "statement is required")
|
|
return
|
|
}
|
|
if t, _ := req.Statement["_type"].(string); t != "https://in-toto.io/Statement/v1" {
|
|
writeErr(w, http.StatusBadRequest, "bad_request", `statement._type must be "https://in-toto.io/Statement/v1"`)
|
|
return
|
|
}
|
|
if pt, _ := req.Statement["predicateType"].(string); pt == "" {
|
|
writeErr(w, http.StatusBadRequest, "bad_request", "statement.predicateType is required")
|
|
return
|
|
}
|
|
|
|
dec, err := s.Policy.Allow(ctx, subject, req.KeyRef)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "policy_eval_error", err.Error())
|
|
return
|
|
}
|
|
if !dec.Allow {
|
|
writeErr(w, http.StatusForbidden, "signing_denied", dec.Reason)
|
|
return
|
|
}
|
|
|
|
signed, err := signer.SignStatement(ctx, s.Signer, req.KeyRef, req.Statement)
|
|
if err != nil {
|
|
if errors.Is(err, signer.ErrUnknownKey) {
|
|
writeErr(w, http.StatusBadRequest, "unknown_key", err.Error())
|
|
return
|
|
}
|
|
if errors.Is(err, signer.ErrNotWired) {
|
|
writeErr(w, http.StatusServiceUnavailable, "signer_unavailable", err.Error())
|
|
return
|
|
}
|
|
writeErr(w, http.StatusInternalServerError, "sign_failed", err.Error())
|
|
return
|
|
}
|
|
|
|
if s.Logger != nil {
|
|
s.Logger.Info("signed", "subject", subject, "key_ref", req.KeyRef, "content_hash", signed.ContentHash)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
if s.Now != nil {
|
|
now = s.Now()
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(signResponse{
|
|
Envelope: signed.Envelope,
|
|
ContentHash: signed.ContentHash,
|
|
SignedAt: now,
|
|
})
|
|
}
|
|
|
|
func writeErr(w http.ResponseWriter, status int, code, message string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(errResponse{Error: errBody{Code: code, Message: message}})
|
|
}
|