Files
attesto-go/proofstream_parity_test.go
Codex a46be8085f sdk(P1.1): offline receipt verification in Python and TypeScript
Closes the trust-model gap where Python/TS could only verify receipts by
calling the server (asking the party being distrusted). Both now verify
entirely client-side, mirroring the Go SDK's VerifyReceiptOffline one-to-one
with identical problem strings so reports are comparable cross-language.

- Python: new attesto.verify.verify_receipt + frozen VerifyReport dataclass,
  using cryptography>=42 (new dependency; not PyNaCl) for Ed25519.
- TypeScript: verifyReceipt via WebCrypto subtle.verify({name:"Ed25519"}),
  throwing a clear AttestoError on runtimes without Ed25519 (Node < 20) rather
  than silently falling back to the server.
Both recompute domain_hash("attesto.v2.receipt", payload) and verify the
signature over domain + 0x00 + canonical_json_bytes(payload), reusing the
frozen canonical functions.

New corpus golden-vectors/sdk-parity/receipts.json (valid + payload/hash/
signature/wrong-key negatives). Proven: all five cases agree across Go,
Python, and TypeScript. READMEs document the offline function and note the
existing client.verify_receipt as the server-assisted variant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:06:53 +02:00

133 lines
3.8 KiB
Go

package attesto
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"testing"
)
type parityVectors struct {
Accept []struct {
ID string `json:"id"`
Payload map[string]any `json:"payload"`
CanonicalPayloadHash string `json:"canonical_payload_hash"`
} `json:"accept"`
Reject []struct {
ID string `json:"id"`
Payload map[string]any `json:"payload"`
Path string `json:"path"`
} `json:"reject"`
}
func loadParityVectors(t *testing.T) parityVectors {
t.Helper()
path := filepath.Join("..", "..", "golden-vectors", "sdk-parity", "canonical-numbers.json")
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read parity vectors: %v", err)
}
var v parityVectors
if err := json.Unmarshal(raw, &v); err != nil {
t.Fatalf("decode parity vectors: %v", err)
}
return v
}
func TestParityAcceptCommitmentHashes(t *testing.T) {
for _, c := range loadParityVectors(t).Accept {
commitment, err := PayloadCommitment(c.Payload)
if err != nil {
t.Fatalf("%s: PayloadCommitment: %v", c.ID, err)
}
if commitment["hash_alg"] != "sha256" {
t.Errorf("%s: hash_alg %q", c.ID, commitment["hash_alg"])
}
if commitment["canonical_payload_hash"] != c.CanonicalPayloadHash {
t.Errorf("%s: hash %s != %s", c.ID, commitment["canonical_payload_hash"], c.CanonicalPayloadHash)
}
}
}
func TestParityAcceptPassPreflight(t *testing.T) {
for _, c := range loadParityVectors(t).Accept {
if err := AssertCommitmentSafeNumbers(c.Payload, "$"); err != nil {
t.Errorf("%s: unexpected preflight error: %v", c.ID, err)
}
}
}
func TestParityRejectPaths(t *testing.T) {
for _, c := range loadParityVectors(t).Reject {
err := AssertCommitmentSafeNumbers(c.Payload, "$")
var ue *UnsafeNumberError
if !errors.As(err, &ue) {
t.Errorf("%s: expected *UnsafeNumberError, got %v", c.ID, err)
continue
}
if ue.Path != c.Path {
t.Errorf("%s: path %s != %s", c.ID, ue.Path, c.Path)
}
}
}
type receiptParityVectors struct {
Cases []struct {
ID string `json:"id"`
PublicKeyHex string `json:"public_key_hex"`
Receipt SignedReceipt `json:"receipt"`
ExpectOK bool `json:"expect_ok"`
ExpectProblem []string `json:"expect_problems"`
} `json:"cases"`
}
func TestParityReceiptVerification(t *testing.T) {
path := filepath.Join("..", "..", "golden-vectors", "sdk-parity", "receipts.json")
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read receipt vectors: %v", err)
}
var v receiptParityVectors
if err := json.Unmarshal(raw, &v); err != nil {
t.Fatalf("decode receipt vectors: %v", err)
}
for _, c := range v.Cases {
report := VerifyReceiptOffline(c.Receipt, c.PublicKeyHex)
if report.OK != c.ExpectOK {
t.Errorf("%s: ok=%v want %v (problems=%v)", c.ID, report.OK, c.ExpectOK, report.Problems)
}
if len(report.Problems) != len(c.ExpectProblem) {
t.Errorf("%s: problems=%v want %v", c.ID, report.Problems, c.ExpectProblem)
continue
}
for i, p := range c.ExpectProblem {
if report.Problems[i] != p {
t.Errorf("%s: problem[%d]=%q want %q", c.ID, i, report.Problems[i], p)
}
}
}
}
func TestParityVerifyPayloadCommitment(t *testing.T) {
for _, c := range loadParityVectors(t).Accept {
commitment, err := PayloadCommitment(c.Payload)
if err != nil {
t.Fatalf("%s: PayloadCommitment: %v", c.ID, err)
}
stored := map[string]any{}
for k, v := range commitment {
stored[k] = v
}
event := map[string]any{"envelope": map[string]any{"payload_commitment": stored}}
ok, err := VerifyPayloadCommitment(c.Payload, event)
if err != nil || !ok {
t.Errorf("%s: verify match ok=%v err=%v", c.ID, ok, err)
}
bad, err := VerifyPayloadCommitment(map[string]any{"tampered": float64(1)}, event)
if err != nil || bad {
t.Errorf("%s: tampered verify bad=%v err=%v", c.ID, bad, err)
}
}
}