Adds payload_commitment / metadata_commitment / verify_payload_commitment and assert_commitment_safe_numbers to the Python, TypeScript, and Go SDKs, each building on the frozen canonical_json/domain_hash primitives (no change to their byte output). The number preflight is a byte-for-byte port of the backend assert_commitment_safe_numbers (floats rejected, |int| > 2^53-1 rejected, bool exempt) and is wired into the v2 log_event / log_events send path, raising a typed AttestoUnsafeNumberError with the JSON path so the rule fails at dev time rather than as a production 422; preflight=False / SkipPreflight defers to the server. New shared corpus golden-vectors/sdk-parity/canonical-numbers.json (15 accept + 8 reject), accept-hashes generated from the backend _commitment. Proven: Python = TypeScript = Go = backend produce byte-identical commitment hashes for every accept vector and identical reject paths (the Go float64-vs-Python- int serialization parity holds). READMEs updated per SDK. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
96 lines
2.7 KiB
Go
96 lines
2.7 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|