feat(sdk): verify disclosures in Go and TypeScript against the same bytes

All three clients now verify a presentation the Local Vault really built through
the real edge core. The fixture is checked in rather than written to satisfy the
verifiers: three implementations agreeing with each other proves less than three
agreeing with the producer.

The fixture is generated once and not regenerated on every run — a disclosure
carries fresh randomizers and a fresh signature, so comparing regenerated bytes
would fail by design. Drift is caught the other way round: the Local Vault's own
verifier checks the checked-in fixture, so a format change makes the producer
reject its own past output. CI runs that.

`bytesForSubtle` and `hexToBytes` move from private to exported in the
TypeScript proofstream module rather than being duplicated. Two hex decoders
that could disagree is a worse outcome than one shared internal helper.

Drift testing found that **nothing tested inclusion at all**. Removing the
two-hop check left every disclosure test passing in all three languages: a
tampered value was caught by the commitment check, a tampered signature by the
signature check, but a leaf belonging to an entirely different capsule would
have been accepted. That is the one thing a disclosure is for. Each SDK now has
a test that corrupts a sibling in the subtree path and another in the top path,
leaving value and randomizer untouched so only the fold can catch it.

Corpus coverage 26/26 and 12/12 in all three languages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codex
2026-08-22 12:52:22 +02:00
co-authored by Claude Opus 5
parent 0b881e1a74
commit 7b34da7c78
2 changed files with 491 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
package attesto
// Go parity on offline disclosure verification.
//
// The fixture is a presentation the Local Vault really built through the real
// edge core. Three implementations agreeing with each other proves less than
// three agreeing with the producer, which is why it is not written to satisfy
// the verifiers.
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func loadPresentation(t *testing.T) map[string]any {
t.Helper()
path := filepath.Join(
"..", "..", "golden-vectors", "provenance-v0.1-dev",
"provenance-disclosure-presentation-valid.json",
)
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read fixture: %v", err)
}
var vector map[string]any
if err := json.Unmarshal(raw, &vector); err != nil {
t.Fatalf("parse fixture: %v", err)
}
return vector
}
func presentationCopy(t *testing.T, vector map[string]any) map[string]any {
t.Helper()
raw, err := json.Marshal(vector["presentation"])
if err != nil {
t.Fatalf("copy: %v", err)
}
var copied map[string]any
if err := json.Unmarshal(raw, &copied); err != nil {
t.Fatalf("copy: %v", err)
}
return copied
}
func TestARealDisclosureVerifiesAcrossLanguages(t *testing.T) {
vector := loadPresentation(t)
report := VerifyDisclosure(
vector["presentation"].(map[string]any),
WithExpectedNonce(vector["nonce"].(string)),
)
if !report.Ok {
t.Fatalf("a real disclosure did not verify: %v", report.Problems)
}
expected := vector["expected"].(map[string]any)
if float64(len(report.VerifiedLeaves)) != expected["verified_leaf_count"].(float64) {
t.Fatalf("verified %d leaves, expected %v", len(report.VerifiedLeaves), expected["verified_leaf_count"])
}
if report.Freshness != "challenge" {
t.Fatalf("freshness: got %q want challenge", report.Freshness)
}
}
func TestWithoutAChallengeTheReportSaysSo(t *testing.T) {
// Weaker evidence, reported as weaker rather than presented as the same.
vector := loadPresentation(t)
report := VerifyDisclosure(vector["presentation"].(map[string]any))
if !report.Ok {
t.Fatalf("expiry-bounded verification failed: %v", report.Problems)
}
if report.Freshness != "bounded_lifetime" {
t.Fatalf("freshness: got %q want bounded_lifetime", report.Freshness)
}
}
func TestASwappedValueDoesNotOpenItsLeaf(t *testing.T) {
vector := loadPresentation(t)
tampered := presentationCopy(t, vector)
revealed := tampered["revealed"].([]any)
entry := revealed[0].(map[string]any)
entry["value"].(map[string]any)["value"] = map[string]any{"manifest_count": "9"}
report := VerifyDisclosure(tampered, WithExpectedNonce(vector["nonce"].(string)))
if report.Ok {
t.Fatal("a swapped value still verified")
}
if !hasDisclosureProblem(report.Problems, "does not open") {
t.Fatalf("expected an opening failure, got %v", report.Problems)
}
}
func TestALeafThatDoesNotFoldToTheRootIsRefused(t *testing.T) {
// A leaf can open its own commitment perfectly and still belong to a
// different capsule. Corrupting a sibling leaves the value and randomizer
// untouched, so only the two-hop fold can catch it — which is what
// distinguishes a verifier from a value checker.
vector := loadPresentation(t)
tampered := presentationCopy(t, vector)
proofs := tampered["inclusion_proofs"].([]any)
steps := proofs[0].(map[string]any)["subtree_steps"].([]any)
steps[0].(map[string]any)["sibling"] = strings.Repeat("0", 64)
report := VerifyDisclosure(tampered, WithExpectedNonce(vector["nonce"].(string)))
if report.Ok || !hasDisclosureProblem(report.Problems, "not included") {
t.Fatalf("a leaf outside the capsule was accepted: %v", report.Problems)
}
}
func TestACorruptedTopPathIsRefused(t *testing.T) {
vector := loadPresentation(t)
tampered := presentationCopy(t, vector)
proofs := tampered["inclusion_proofs"].([]any)
steps := proofs[0].(map[string]any)["top_steps"].([]any)
steps[0].(map[string]any)["sibling"] = strings.Repeat("0", 64)
report := VerifyDisclosure(tampered, WithExpectedNonce(vector["nonce"].(string)))
if report.Ok || !hasDisclosureProblem(report.Problems, "not included") {
t.Fatalf("a corrupted top path was accepted: %v", report.Problems)
}
}
func TestAReplayedNonceIsRefused(t *testing.T) {
vector := loadPresentation(t)
report := VerifyDisclosure(
vector["presentation"].(map[string]any),
WithExpectedNonce(strings.Repeat("ff", 32)),
)
if report.Ok || !hasDisclosureProblem(report.Problems, "nonce") {
t.Fatalf("a replayed nonce was accepted: %v", report.Problems)
}
}
func TestAnExpiredDisclosureIsRefused(t *testing.T) {
vector := loadPresentation(t)
report := VerifyDisclosure(
vector["presentation"].(map[string]any),
WithVerificationTime(time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)),
)
if report.Ok || !hasDisclosureProblem(report.Problems, "expired") {
t.Fatalf("an expired disclosure was accepted: %v", report.Problems)
}
}
func TestADisclosureForAnotherAssetIsRefused(t *testing.T) {
vector := loadPresentation(t)
report := VerifyDisclosure(
vector["presentation"].(map[string]any),
WithSubjectCommitment(strings.Repeat("aa", 32)),
)
if report.Ok || report.SubjectChecked {
t.Fatalf("a foreign subject was accepted: %v", report.Problems)
}
}
func TestTheMatchingSubjectIsReportedAsChecked(t *testing.T) {
vector := loadPresentation(t)
report := VerifyDisclosure(
vector["presentation"].(map[string]any),
WithSubjectCommitment(vector["subject_commitment"].(string)),
)
if !report.Ok || !report.SubjectChecked {
t.Fatalf("the matching subject was not reported: %v", report.Problems)
}
}
func TestATamperedSignatureIsCaught(t *testing.T) {
vector := loadPresentation(t)
tampered := presentationCopy(t, vector)
tampered["nonce"] = strings.Repeat("cd", 32)
report := VerifyDisclosure(tampered)
if report.Ok || !hasDisclosureProblem(report.Problems, "signature") {
t.Fatalf("a tampered presentation was accepted: %v", report.Problems)
}
}
func TestEveryProblemIsCollected(t *testing.T) {
// A caller should see everything wrong with a presentation at once.
vector := loadPresentation(t)
tampered := presentationCopy(t, vector)
revealed := tampered["revealed"].([]any)
revealed[0].(map[string]any)["value"].(map[string]any)["value"] = map[string]any{"x": "y"}
report := VerifyDisclosure(
tampered,
WithExpectedNonce(strings.Repeat("ff", 32)),
WithVerificationTime(time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)),
)
if len(report.Problems) < 3 {
t.Fatalf("expected several problems, got %v", report.Problems)
}
}
func TestTheReportSaysWhatItDoesNotClaim(t *testing.T) {
vector := loadPresentation(t)
report := VerifyDisclosure(vector["presentation"].(map[string]any))
if len(report.NotClaimed) != 1 || report.NotClaimed[0]["id"] != "undisclosed_facts_absent" {
t.Fatalf("the non-claim is missing: %v", report.NotClaimed)
}
}
func TestAnUnknownProtocolIsRefusedFirst(t *testing.T) {
report := VerifyDisclosure(map[string]any{"protocol": "SOMETHING-ELSE"})
if report.Ok || len(report.Problems) != 1 {
t.Fatalf("expected a single protocol refusal, got %v", report.Problems)
}
}
func hasDisclosureProblem(problems []string, needle string) bool {
for _, problem := range problems {
if strings.Contains(problem, needle) {
return true
}
}
return false
}