feat(attesto3): make every SDK check every vector it is able to check

Twelve of twenty-five provenance vectors were consumed by no SDK. From outside
the repository that looked exactly like full coverage, which is the problem: a
corpus proves nothing about an implementation that never loads it.

Vectors now declare what they require. `sha256` vectors use SHA-256 and
canonical JSON, which all three SDKs have, so an unconsumed one is a gap and
fails a contract. `ristretto255` vectors need curve scalar arithmetic no SDK
carries; those are a declared boundary with a stated reason rather than a silent
skip, so the exemption cannot spread by habit.

Coverage went from 13/17 reachable in each SDK to 17/17. Closing the four gaps
surfaced a real verifier weakness: `capsule_root` receives digests, so by then a
role is no longer visible, and a tree carrying `evidence_root` twice with
`vault_identity_commitment` missing folds to a root all three SDKs accepted.
Each gains `ordered_top_leaf_digests`, which requires each of the six roles
exactly once, and the safe path is now the easy one.

Two findings of my own drift:

* The Go corpus-typing test accepted only `valid` and `invalid`, so it had been
  failing since the Sprint 1 recovery added vectors carrying `differs` and
  `rejected`. I updated Python's typing test then and not Go's, and no gate
  caught it because the SDK parity suites are not in the sprint gates. Fixed,
  and both Go and TypeScript now also require the capability declaration.
* TypeScript's strict indexing caught that a missing randomizer would have
  reached the hash as the string "undefined". Both halves are now checked.

Python 88, Go ok, TypeScript 107, Local Vault 375, edge 116, backend 1404.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codex
2026-08-21 16:36:13 +02:00
co-authored by Claude Opus 5
parent f80b28c3b5
commit 496d622671
2 changed files with 206 additions and 5 deletions
+37 -3
View File
@@ -48,9 +48,9 @@ var ProvenanceDomains = map[string]struct{}{
"attesto.provenance.v1.evidence_tree": {},
"attesto.provenance.v1.policy_tree": {},
"attesto.provenance.v1.attestation": {},
"attesto.disclosure.v2": {},
"attesto.zk.range.v1.statement": {},
"attesto.zk.range.v1.transcript": {},
"attesto.disclosure.v2": {},
"attesto.zk.range.v1.statement": {},
"attesto.zk.range.v1.transcript": {},
}
// TopLeafRoles is the canonical order of the six typed top-tree leaves. No
@@ -263,6 +263,40 @@ func TopLeafDigest(leafRole, commitment, randomizer string) (string, error) {
})
}
// OrderedTopLeafDigests builds the six typed top leaves, requiring each role
// exactly once.
//
// CapsuleRoot receives digests, so by then a role is no longer visible and a
// tree carrying evidence_root twice with vault_identity_commitment missing folds
// to a root it will accept. The check has to happen here, where the roles still
// exist, which is also why callers should reach for this rather than assembling
// the slice themselves.
func OrderedTopLeafDigests(commitments, randomizers map[string]string) ([]string, error) {
for _, role := range TopLeafRoles {
if _, ok := commitments[role]; !ok {
return nil, fmt.Errorf("capsule tree is missing top leaf %s", role)
}
}
known := make(map[string]struct{}, len(TopLeafRoles))
for _, role := range TopLeafRoles {
known[role] = struct{}{}
}
for role := range commitments {
if _, ok := known[role]; !ok {
return nil, fmt.Errorf("unknown top leaf role: %s", role)
}
}
digests := make([]string, 0, len(TopLeafRoles))
for _, role := range TopLeafRoles {
digest, err := TopLeafDigest(role, commitments[role], randomizers[role])
if err != nil {
return nil, err
}
digests = append(digests, digest)
}
return digests, nil
}
// CapsuleRoot folds the six typed top leaves into the capsule root.
func CapsuleRoot(orderedTopLeafDigests []string) (string, error) {
if len(orderedTopLeafDigests) != len(TopLeafRoles) {