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>
219 lines
7.1 KiB
Go
219 lines
7.1 KiB
Go
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
|
|
}
|