feat(attesto3): let a verifier client say what it did not check

Sprint 12's own evidence named this the feature's largest risk: not a broken
proof, but a correct proof rendered as "AI generated: true". The SDKs had no
result surface at all — only cryptographic primitives — so nothing stopped a
consumer from reading a predicate result however it liked.

All three now carry `inspect_predicate_result`, and the rule that shapes it is
that a client without ristretto255 arithmetic cannot verify a range proof and
must say so. `verified_here.zk_predicate` is always `not_checked`; the issuer's
own verification block is carried separately under `reported_by_issuer`. An SDK
that merged the two would be passing the prover's word through as though it had
confirmed it, which is the failure the whole construction exists to prevent.
Inclusion, which is SHA-256, is reported as genuinely checked when the caller
checked it.

A result is refused outright if it omits one of the three non-claims or carries
a field a consumer could render as a verdict — at any nesting depth, since
`predicate.confidence` misleads exactly as well as a top-level one.

The corpus is generated by calling the real evaluator, so the fixture cannot
drift from the implementation, and a contract compares rather than regenerates.

Extending the coverage contract to a second corpus surfaced a third one:
`provenance-envelope-v0.1` matched nothing. It turned out to be guarded a
different but equally strict way — its own contract pins an explicit inventory —
so the contract now models both shapes. "Checked somewhere else" and "checked by
nobody" can no longer look the same, and a new corpus fails until one model or
the other covers it.

Python 100, Go ok, TypeScript 115, Local Vault 375.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codex
2026-08-21 18:17:14 +02:00
co-authored by Claude Opus 5
parent 496d622671
commit c31c1796ae
2 changed files with 295 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
package attesto
// Go parity against the ATTESTO-ZK-RANGE-001 result corpus.
//
// The cryptography of a range proof is not checked here and cannot be: this SDK
// carries no ristretto255 arithmetic. What is checked is what an SDK can get
// wrong on its own — reporting the issuer's word as its own finding, or handing
// a consumer an object shaped like a verdict.
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func zkRangeVectorDir(t *testing.T) string {
t.Helper()
dir := filepath.Join("..", "..", "golden-vectors", "zk-range-v0.1-dev")
if _, err := os.Stat(dir); err != nil {
t.Fatalf("no zk-range vectors at %s: %v", dir, err)
}
return dir
}
func loadZKRangeVector(t *testing.T, name string) map[string]any {
t.Helper()
raw, err := os.ReadFile(filepath.Join(zkRangeVectorDir(t), name+".json"))
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
var vector map[string]any
if err := json.Unmarshal(raw, &vector); err != nil {
t.Fatalf("parse %s: %v", name, err)
}
return vector
}
func TestZKRangeCorpusIsPresentAndTyped(t *testing.T) {
entries, err := filepath.Glob(filepath.Join(zkRangeVectorDir(t), "*.json"))
if err != nil || len(entries) == 0 {
t.Fatalf("no zk-range vectors: %v", err)
}
for _, entry := range entries {
raw, err := os.ReadFile(entry)
if err != nil {
t.Fatalf("read %s: %v", entry, err)
}
var vector map[string]any
if err := json.Unmarshal(raw, &vector); err != nil {
t.Fatalf("parse %s: %v", entry, err)
}
if vector["protocol"] != ZKRangeProtocol {
t.Fatalf("%s: wrong protocol %v", entry, vector["protocol"])
}
if vector["requires"] == nil {
t.Fatalf("%s: does not declare what it requires", entry)
}
switch vector["expectation"] {
case "valid", "invalid", "rejected", "differs":
default:
t.Fatalf("%s: bad expectation %v", entry, vector["expectation"])
}
}
}
func TestZKRangeClientWithoutCurveArithmeticSaysSo(t *testing.T) {
// The whole point. This SDK cannot verify the proof and reports that; the
// issuer's own verification block is kept under a separate key so a reader
// can tell a claim apart from a check.
vector := loadZKRangeVector(t, "zk-range-result-valid")
result := vector["result"].(map[string]any)
report, err := InspectPredicateResult(result, nil)
if err != nil {
t.Fatalf("inspect: %v", err)
}
if report.VerifiedHere["zk_predicate"] != "not_checked" {
t.Fatalf("this client cannot verify a proof but reported %q", report.VerifiedHere["zk_predicate"])
}
expected := vector["expected_verified_here"].(map[string]any)
for key, want := range expected {
if report.VerifiedHere[key] != want.(string) {
t.Fatalf("verified_here[%s]: got %q want %v", key, report.VerifiedHere[key], want)
}
}
if report.ReportedByIssuer["zk_predicate"] != "verified" {
t.Fatalf("the issuer's own claim was not carried through separately")
}
}
func TestZKRangeInclusionTheClientCheckedIsReported(t *testing.T) {
// Not everything is out of reach: two-hop inclusion is SHA-256.
vector := loadZKRangeVector(t, "zk-range-result-valid")
result := vector["result"].(map[string]any)
for _, testCase := range []struct {
checked bool
want string
}{{true, "verified"}, {false, "failed"}} {
checked := testCase.checked
report, err := InspectPredicateResult(result, &checked)
if err != nil {
t.Fatalf("inspect: %v", err)
}
if report.VerifiedHere["capsule_inclusion"] != testCase.want {
t.Fatalf("capsule_inclusion: got %q want %q", report.VerifiedHere["capsule_inclusion"], testCase.want)
}
}
}
func TestZKRangeResultCarriesTheThreeNonClaims(t *testing.T) {
vector := loadZKRangeVector(t, "zk-range-result-valid")
report, err := InspectPredicateResult(vector["result"].(map[string]any), nil)
if err != nil {
t.Fatalf("inspect: %v", err)
}
if len(report.NotClaimed) != len(RequiredNonClaims) {
t.Fatalf("expected %d non-claims, got %d", len(RequiredNonClaims), len(report.NotClaimed))
}
for index, required := range RequiredNonClaims {
if report.NotClaimed[index]["id"] != required {
t.Fatalf("non-claim %d: got %v want %s", index, report.NotClaimed[index]["id"], required)
}
}
}
func TestZKRangeMisleadingResultsAreRefused(t *testing.T) {
for _, name := range []string{
"zk-range-result-missing-non-claim",
"zk-range-result-verdict-field",
"zk-range-result-nested-verdict-field",
"zk-range-result-unbound",
"zk-range-result-unsupported-version",
} {
vector := loadZKRangeVector(t, name)
if vector["expectation"] != "rejected" {
t.Fatalf("%s: expected a rejected vector", name)
}
if _, err := InspectPredicateResult(vector["result"].(map[string]any), nil); err == nil {
t.Fatalf("%s was accepted", name)
}
}
}
func TestZKRangeRefusalsAreNotBlanket(t *testing.T) {
// The refusals above must not be a function that refuses everything.
vector := loadZKRangeVector(t, "zk-range-result-valid")
if _, err := InspectPredicateResult(vector["result"].(map[string]any), nil); err != nil {
t.Fatalf("a well-formed result was refused: %v", err)
}
}