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
+143
View File
@@ -18,6 +18,7 @@ import (
"encoding/hex"
"fmt"
"sort"
"strings"
)
const (
@@ -430,3 +431,145 @@ func EnvelopeCoreCanonicalBytes(envelope map[string]any) ([]byte, error) {
}
return CanonicalJSON(core)
}
// ---------------------------------------------------------------- predicates
const (
ZKRangeProtocol = "ATTESTO-ZK-RANGE-001"
ZKRangeProtocolVersion = "0.1"
PredicateResultSchema = "attesto.provenance.predicate_result"
PredicateResultSchemaVersion = "0.1"
)
// RequiredNonClaims must appear in every predicate result. A result that dropped
// one would be read as the stronger statement, which is the failure this
// vocabulary prevents.
var RequiredNonClaims = [3]string{
"detector_correctness_not_proven",
"content_truth_not_proven",
"ai_generation_not_proven",
}
// forbiddenResultFields would let a consumer render a proven bound as a verdict
// about the content. A range proof says a named detector's measurement fell
// inside an interval and nothing more.
var forbiddenResultFields = map[string]struct{}{
"ai_generated": {}, "synthetic": {}, "is_fake": {}, "authentic": {},
"confidence": {}, "score": {}, "probability": {},
}
// PredicateReport separates what this client checked from what the issuer claims.
type PredicateReport struct {
Protocol string `json:"protocol"`
CapsuleRoot string `json:"capsule_root"`
ClaimID string `json:"claim_id"`
CommitmentC string `json:"commitment_c"`
Predicate map[string]any `json:"predicate"`
VerifiedHere map[string]string `json:"verified_here"`
ReportedByIssuer map[string]any `json:"reported_by_issuer"`
NotClaimed []map[string]any `json:"not_claimed"`
}
func rejectVerdictFields(node any, path string) error {
switch typed := node.(type) {
case map[string]any:
for key, value := range typed {
if _, bad := forbiddenResultFields[strings.ToLower(key)]; bad {
return fmt.Errorf("predicate result carries a verdict-shaped field at %s.%s", path, key)
}
if err := rejectVerdictFields(value, path+"."+key); err != nil {
return err
}
}
case []any:
for index, value := range typed {
if err := rejectVerdictFields(value, fmt.Sprintf("%s[%d]", path, index)); err != nil {
return err
}
}
}
return nil
}
// InspectPredicateResult reports what this SDK established, kept apart from what
// the issuer claims.
//
// This is a verification client without ristretto255 arithmetic, so it cannot
// check a range proof. It says not_checked rather than passing the issuer's word
// through as though it had verified it: an SDK that reported the issuer's
// "verified" as its own is the failure this construction exists to prevent.
//
// capsuleInclusion is nil when the caller did not check inclusion.
func InspectPredicateResult(result map[string]any, capsuleInclusion *bool) (*PredicateReport, error) {
if result["schema"] != PredicateResultSchema || result["schema_version"] != PredicateResultSchemaVersion {
return nil, fmt.Errorf("unsupported predicate result schema")
}
if result["protocol"] != ZKRangeProtocol || result["protocol_version"] != ZKRangeProtocolVersion {
return nil, fmt.Errorf("unsupported predicate protocol")
}
rawClaims, _ := result["not_claimed"].([]any)
declared := map[string]struct{}{}
notClaimed := make([]map[string]any, 0, len(rawClaims))
for _, raw := range rawClaims {
claim, ok := raw.(map[string]any)
if !ok {
return nil, fmt.Errorf("predicate result carries a malformed non-claim")
}
if id, ok := claim["id"].(string); ok {
declared[id] = struct{}{}
}
copied := map[string]any{}
for key, value := range claim {
copied[key] = value
}
notClaimed = append(notClaimed, copied)
}
for _, required := range RequiredNonClaims {
if _, ok := declared[required]; !ok {
return nil, fmt.Errorf("predicate result omits required non-claims: %s", required)
}
}
if err := rejectVerdictFields(result, "result"); err != nil {
return nil, err
}
bound := map[string]string{}
for _, field := range []string{"capsule_root", "claim_id", "commitment_c"} {
value, ok := result[field].(string)
if !ok || value == "" {
return nil, fmt.Errorf("predicate result has no %s to bind to", field)
}
bound[field] = value
}
inclusion := "not_checked"
if capsuleInclusion != nil {
if *capsuleInclusion {
inclusion = "verified"
} else {
inclusion = "failed"
}
}
predicate, _ := result["predicate"].(map[string]any)
issuer, _ := result["verification"].(map[string]any)
return &PredicateReport{
Protocol: ZKRangeProtocol,
CapsuleRoot: bound["capsule_root"],
ClaimID: bound["claim_id"],
CommitmentC: bound["commitment_c"],
Predicate: predicate,
// zk_predicate is always not_checked: verifying the proof needs curve
// arithmetic this client does not carry.
VerifiedHere: map[string]string{
"zk_predicate": "not_checked",
"capsule_inclusion": inclusion,
},
// What the issuer says it checked, kept separate so a reader can tell a
// claim from a check.
ReportedByIssuer: issuer,
NotClaimed: notClaimed,
}, nil
}