package httpapi import ( "bytes" "context" "crypto/ed25519" "crypto/rand" "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "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" ) func mustGenKey(t *testing.T) ed25519.PrivateKey { t.Helper() _, k, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } return k } func newTestServer(t *testing.T) (*httptest.Server, ed25519.PublicKey) { t.Helper() priv := mustGenKey(t) s := signer.NewInMemory() s.AddKey("trust-api/test-key", priv) srv := Server{ Signer: s, Policy: policy.AllowAll{}, Validator: token.Static{"dev-token": "agent-admin-api"}, Now: func() time.Time { return time.Date(2026, 10, 25, 18, 30, 0, 0, time.UTC) }, } mux := http.NewServeMux() srv.Mount(mux) return httptest.NewServer(mux), priv.Public().(ed25519.PublicKey) } func post(url, bearer, body string) (*http.Response, error) { req, _ := http.NewRequest("POST", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") if bearer != "" { req.Header.Set("Authorization", "Bearer "+bearer) } return http.DefaultClient.Do(req) } func TestSign_HappyPath_VerifiesEndToEnd(t *testing.T) { srv, pub := newTestServer(t) defer srv.Close() body := `{ "key_ref":"trust-api/test-key", "statement":{ "_type":"https://in-toto.io/Statement/v1", "subject":[{"name":"per_diem_rate/PDR-1","digest":{"sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}], "predicateType":"https://basicstack.de/attestations/rate_change/v1", "predicate":{"change_kind":"create","rate_id":"PDR-1"} } }` resp, err := post(srv.URL+"/api/v1/sign/dsse", "dev-token", body) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status %d", resp.StatusCode) } var out signResponse if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { t.Fatalf("decode: %v", err) } if !strings.HasPrefix(out.ContentHash, "sha256:") || len(out.ContentHash) != 71 { t.Fatalf("content_hash shape: %q", out.ContentHash) } // Independently verify the signature with the server's public key. payload, err := base64.RawURLEncoding.DecodeString(out.Envelope.Payload) if err != nil { t.Fatalf("decode payload: %v", err) } pae := encodePAEForTest("application/vnd.in-toto+json", payload) sig, err := base64.RawURLEncoding.DecodeString(out.Envelope.Signatures[0].Sig) if err != nil { t.Fatalf("decode sig: %v", err) } if !ed25519.Verify(pub, pae, sig) { t.Fatal("returned signature does not verify against server public key") } } func encodePAEForTest(payloadType string, payload []byte) []byte { // Mirror of signer.encodePAE — kept local so this test does not depend on // an unexported helper and so a drift between client and server would // surface here. s := "DSSEv1 " s += itoa(len(payloadType)) + " " + payloadType + " " + itoa(len(payload)) + " " return append([]byte(s), payload...) } func itoa(n int) string { if n == 0 { return "0" } var buf [20]byte i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } return string(buf[i:]) } func TestSign_RejectsMissingBearer(t *testing.T) { srv, _ := newTestServer(t) defer srv.Close() resp, _ := post(srv.URL+"/api/v1/sign/dsse", "", "{}") defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", resp.StatusCode) } } func TestSign_RejectsUnknownBearer(t *testing.T) { srv, _ := newTestServer(t) defer srv.Close() resp, _ := post(srv.URL+"/api/v1/sign/dsse", "bogus", "{}") defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", resp.StatusCode) } } func TestSign_RejectsUnknownKeyRef(t *testing.T) { srv, _ := newTestServer(t) defer srv.Close() body := `{"key_ref":"nope","statement":{"_type":"https://in-toto.io/Statement/v1","predicateType":"x"}}` resp, _ := post(srv.URL+"/api/v1/sign/dsse", "dev-token", body) defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } var out errResponse _ = json.NewDecoder(resp.Body).Decode(&out) if out.Error.Code != "unknown_key" { t.Errorf("expected code=unknown_key, got %q (%s)", out.Error.Code, out.Error.Message) } } type denyAll struct{} func (denyAll) Allow(_ context.Context, _, _ string) (policy.Decision, error) { return policy.Decision{Allow: false, Reason: "no such key allowed for this client"}, nil } func TestSign_RejectsOnPolicyDeny(t *testing.T) { priv := mustGenKey(t) s := signer.NewInMemory() s.AddKey("k", priv) srv := Server{ Signer: s, Policy: denyAll{}, Validator: token.Static{"dev-token": "agent-x"}, } mux := http.NewServeMux() srv.Mount(mux) hs := httptest.NewServer(mux) defer hs.Close() body := `{"key_ref":"k","statement":{"_type":"https://in-toto.io/Statement/v1","predicateType":"p"}}` resp, _ := post(hs.URL+"/api/v1/sign/dsse", "dev-token", body) defer resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Fatalf("expected 403, got %d", resp.StatusCode) } var out errResponse _ = json.NewDecoder(resp.Body).Decode(&out) if out.Error.Code != "signing_denied" { t.Errorf("expected signing_denied, got %q", out.Error.Code) } } func TestSign_RejectsBadStatement(t *testing.T) { srv, _ := newTestServer(t) defer srv.Close() bad := []string{ `{"key_ref":"trust-api/test-key","statement":{}}`, `{"key_ref":"trust-api/test-key","statement":{"_type":"wrong"}}`, `{"key_ref":"trust-api/test-key","statement":{"_type":"https://in-toto.io/Statement/v1"}}`, `{"key_ref":"","statement":{"_type":"https://in-toto.io/Statement/v1","predicateType":"p"}}`, } for i, b := range bad { resp, _ := post(srv.URL+"/api/v1/sign/dsse", "dev-token", b) if resp.StatusCode != http.StatusBadRequest { t.Errorf("case %d: expected 400, got %d", i, resp.StatusCode) } resp.Body.Close() } } func TestHealth(t *testing.T) { srv, _ := newTestServer(t) defer srv.Close() resp, err := http.Get(srv.URL + "/healthz") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != 200 { t.Fatalf("status %d", resp.StatusCode) } }