package attest import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "time" ) // HTTPSigner calls POST /api/v1/sign/dsse on the Signing Service (ADR-0005). // Wire shape matches admin-api/internal/attest.HTTPSigner so the two services // are drop-in interchangeable against the same Signing Service deployment. type HTTPSigner struct { BaseURL string Client *http.Client TokenSource TokenSource } type signRequest struct { KeyRef string `json:"key_ref"` Statement Statement `json:"statement"` } type signResponse struct { Envelope Envelope `json:"envelope"` ContentHash string `json:"content_hash"` SignedAt time.Time `json:"signed_at"` } func (s HTTPSigner) SignDSSE(ctx context.Context, keyRef string, stmt Statement) (Envelope, string, error) { if s.BaseURL == "" { return Envelope{}, "", errors.New("attest.HTTPSigner: BaseURL is empty") } cli := s.Client if cli == nil { cli = &http.Client{Timeout: 10 * time.Second} } body, err := json.Marshal(signRequest{KeyRef: keyRef, Statement: stmt}) if err != nil { return Envelope{}, "", fmt.Errorf("marshal: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.BaseURL+"/api/v1/sign/dsse", bytes.NewReader(body)) if err != nil { return Envelope{}, "", err } req.Header.Set("Content-Type", "application/json") if s.TokenSource != nil { tok, err := s.TokenSource.Token(ctx) if err != nil { return Envelope{}, "", fmt.Errorf("token: %w", err) } req.Header.Set("Authorization", "Bearer "+tok) } resp, err := cli.Do(req) if err != nil { return Envelope{}, "", err } defer resp.Body.Close() raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if resp.StatusCode != http.StatusOK { return Envelope{}, "", fmt.Errorf("signer: status %d: %s", resp.StatusCode, string(raw)) } var out signResponse if err := json.Unmarshal(raw, &out); err != nil { return Envelope{}, "", fmt.Errorf("decode: %w", err) } if out.ContentHash == "" { return Envelope{}, "", errors.New("signer: response missing content_hash") } return out.Envelope, out.ContentHash, nil }