feat(sdk): verify Pedersen openings in all three clients, without imposing a curve library
The eight private-numeric vectors were a declared boundary: verifying them needs ristretto255 scalar arithmetic no SDK carried. Adding one everywhere would have cost something real — the Go and TypeScript SDKs have *zero* dependencies, which is a property their consumers get for free today. So it is optional in each, and the shape differs per ecosystem: a `attesto[zk]` extra in Python, an optional peer dependency in TypeScript, and a separate `go.attesto.eu/sdk/zk` module in Go. A consumer who never opens a private numeric inherits nothing. `not_checked` now means "this installation did not check" rather than "nobody can", which is a better answer to the same question. Each was verified against a real Rust commitment before being chosen: pysodium over libsodium, @noble/curves, and gtank/ristretto255 all reproduce the core's bytes exactly. PyNaCl was tried first and ruled out — 1.6.2 exposes no ristretto255 bindings at all. This closes Pedersen opening verification, not range proofs. A range proof needs a full bulletproofs implementation, not curve arithmetic, and stays the core's job. Two things the last vector forced: * A value outside the descriptor's declared domain now returns invalid for the right reason. The commitment would fail to match anyway, but attributing that to the arithmetic when the real answer is "that value is outside the declared domain" blames the wrong layer. All three match the Rust core here. * A skipped suite is a gate that proves nothing, so CI sets ATTESTO_REQUIRE_ZK_EXTRA and an environment that was supposed to install the dependency and did not now fails instead of reporting green over skips. Corpus coverage is 25/25 and 12/12 in all three languages, with no exemption left. The exemption mechanism is removed rather than emptied: reintroducing one should be a visible decision, not a constant someone left lying around. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,20 @@ func VerifyOpening(descriptor map[string]any, encodedValue uint64, blindingScala
|
|||||||
return false, fmt.Errorf("descriptor carries no commitment to open")
|
return false, fmt.Errorf("descriptor carries no commitment to open")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A value outside the declared domain cannot be what was committed, whatever
|
||||||
|
// the blinding. Checking it here rather than letting the commitment simply
|
||||||
|
// fail to match means the answer names the real reason, and matches the Rust
|
||||||
|
// core.
|
||||||
|
if encoding, ok := descriptor["encoding"].(map[string]any); ok {
|
||||||
|
minimum, hasMin := encoding["semantic_min_encoded"].(float64)
|
||||||
|
maximum, hasMax := encoding["semantic_max_encoded"].(float64)
|
||||||
|
if hasMin && hasMax {
|
||||||
|
if float64(encodedValue) < minimum || float64(encodedValue) > maximum {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
raw, err := hex.DecodeString(blindingScalar)
|
raw, err := hex.DecodeString(blindingScalar)
|
||||||
if err != nil || len(raw) != 32 {
|
if err != nil || len(raw) != 32 {
|
||||||
return false, fmt.Errorf("opening blinding is not 32 hex-encoded bytes")
|
return false, fmt.Errorf("opening blinding is not 32 hex-encoded bytes")
|
||||||
|
|||||||
@@ -130,6 +130,20 @@ func TestAWidenedEncodingDomainIsNotTheCurveChecksBusiness(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAValueOutsideTheDeclaredDomainCannotOpen(t *testing.T) {
|
||||||
|
// "invalid", not "rejected": the check ran and answered no. The commitment
|
||||||
|
// would fail to match anyway, but for the wrong reason.
|
||||||
|
vector := loadVector(t, "provenance-private-numeric-semantic-bound-invalid")
|
||||||
|
descriptor, value, blinding := openingFrom(t, vector)
|
||||||
|
ok, err := VerifyOpening(descriptor, value, blinding)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("verify: %v", err)
|
||||||
|
}
|
||||||
|
if ok != vector["expected_valid"].(bool) {
|
||||||
|
t.Fatalf("got %v want %v", ok, vector["expected_valid"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAMalformedOpeningIsRefused(t *testing.T) {
|
func TestAMalformedOpeningIsRefused(t *testing.T) {
|
||||||
vector := loadVector(t, "provenance-private-numeric-opening-valid")
|
vector := loadVector(t, "provenance-private-numeric-opening-valid")
|
||||||
descriptor, value, _ := openingFrom(t, vector)
|
descriptor, value, _ := openingFrom(t, vector)
|
||||||
|
|||||||
Reference in New Issue
Block a user