package attest import ( "bytes" "context" "crypto/ed25519" "crypto/rand" "encoding/base64" "net/http/httptest" "strings" "testing" "time" signinghttp "basicstack.de/proofedmesh/services/signing-service/pkg/httpapi" ) // Interop test for DEV-599: the real attest.HTTPSigner client is pointed at a // real signing-service HTTP handler (same code paths production uses, just // backed by the in-memory Ed25519 keyring and AllowAll policy), and we assert // the full ADR-0005 round-trip: // // 1. HTTPSigner POSTs to /api/v1/sign/dsse with (key_ref, statement). // 2. The signing-service canonicalises, builds the DSSE PAE, signs with // Ed25519, assembles the envelope, hashes it. // 3. HTTPSigner receives (envelope, content_hash) and returns them. // 4. The test re-derives content_hash from the envelope bytes to prove // stability, decodes the base64url payload+signature, rebuilds the PAE, // and verifies the Ed25519 signature with the server's public key. // // A green run proves every admin-api consumer of HTTPSigner (admin HTTP // routes, rate_change Orchestrator, future agents) is wire-compatible with // the signing-service as it is actually built today. func TestInterop_HTTPSigner_AgainstRealSigningService(t *testing.T) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } const keyRef = "trust-api/rate-change-signer-2026-q4" handler := signinghttp.NewTestHandler(signinghttp.TestServerConfig{ Keys: map[string]ed25519.PrivateKey{keyRef: priv}, Tokens: map[string]string{"interop-token": "agent-admin-api"}, Now: func() time.Time { return time.Date(2026, 10, 25, 18, 30, 0, 0, time.UTC) }, }) server := httptest.NewServer(handler) t.Cleanup(server.Close) // The real admin-api client, pointed at the real signing-service handler. s := HTTPSigner{BaseURL: server.URL, TokenSource: StaticToken("interop-token")} stmt := Statement{ Type: "https://in-toto.io/Statement/v1", Subject: []Subject{{ Name: "per_diem_rate/PDR-1", Digest: map[string]string{"sha256": "0000000000000000000000000000000000000000000000000000000000000000"}, }}, PredicateType: "https://basicstack.de/attestations/rate_change/v1", Predicate: map[string]any{ "change_kind": "create", "rate_id": "PDR-1", "country": "DE", "currency": "EUR", }, } env, contentHash, err := s.SignDSSE(context.Background(), keyRef, stmt) if err != nil { t.Fatalf("HTTPSigner.SignDSSE against real signing-service: %v", err) } // Envelope shape per ADR-0005. if env.PayloadType != "application/vnd.in-toto+json" { t.Errorf("payloadType: %q", env.PayloadType) } if len(env.Signatures) != 1 { t.Fatalf("want 1 signature, got %d", len(env.Signatures)) } if env.Signatures[0].KeyID != keyRef { t.Errorf("keyid: %q (want %q)", env.Signatures[0].KeyID, keyRef) } if !strings.HasPrefix(contentHash, "sha256:") || len(contentHash) != len("sha256:")+64 { t.Errorf("content_hash shape: %q", contentHash) } // content_hash must be self-consistent: recomputing it from the envelope // bytes via the admin-api's own canonicalisation yields the same value. // This is the stability guarantee ADR-0005 relies on everywhere // `rate_change_attestation_ref` is stored. recomputed, err := EnvelopeContentHash(env) if err != nil { t.Fatal(err) } if recomputed != contentHash { t.Fatalf("content_hash is not self-consistent across admin-api canonicaliser:\n server=%s\n client=%s", contentHash, recomputed) } // Verify the Ed25519 signature end-to-end with the server's public key. payload, err := base64.RawURLEncoding.DecodeString(env.Payload) if err != nil { t.Fatalf("payload decode: %v", err) } sig, err := base64.RawURLEncoding.DecodeString(env.Signatures[0].Sig) if err != nil { t.Fatalf("sig decode: %v", err) } pae := paeDSSEv1(env.PayloadType, payload) if !ed25519.Verify(pub, pae, sig) { t.Fatal("Ed25519.Verify failed on (pae, sig) returned by the signing-service via HTTPSigner") } // The payload bytes are the canonical JSON of the submitted statement. // Any divergence here would mean HTTPSigner and the signing-service // disagree on canonicalisation, which breaks every downstream verifier. expectedPayload, err := canonicalJSON(stmt) if err != nil { t.Fatal(err) } if !bytes.Equal(payload, expectedPayload) { t.Fatalf("payload bytes diverge:\n got=%s\nwant=%s", string(payload), string(expectedPayload)) } } // paeDSSEv1 mirrors the signing-service's PAE encoder. Kept local to this // test so a drift between client and server would surface here instead of // relying on an unexported helper on either side. func paeDSSEv1(payloadType string, payload []byte) []byte { 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:]) }