ProofedMesh/services/admin-api/internal/attest/interop_signing_test.go

142 lines
4.9 KiB
Go
Raw Normal View History

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:])
}