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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user