feat(sdk): verify disclosures in Go and TypeScript against the same bytes
All three clients now verify a presentation the Local Vault really built through the real edge core. The fixture is checked in rather than written to satisfy the verifiers: three implementations agreeing with each other proves less than three agreeing with the producer. The fixture is generated once and not regenerated on every run — a disclosure carries fresh randomizers and a fresh signature, so comparing regenerated bytes would fail by design. Drift is caught the other way round: the Local Vault's own verifier checks the checked-in fixture, so a format change makes the producer reject its own past output. CI runs that. `bytesForSubtle` and `hexToBytes` move from private to exported in the TypeScript proofstream module rather than being duplicated. Two hex decoders that could disagree is a worse outcome than one shared internal helper. Drift testing found that **nothing tested inclusion at all**. Removing the two-hop check left every disclosure test passing in all three languages: a tampered value was caught by the commitment check, a tampered signature by the signature check, but a leaf belonging to an entirely different capsule would have been accepted. That is the one thing a disclosure is for. Each SDK now has a test that corrupts a sibling in the subtree path and another in the top path, leaving value and randomizer untouched so only the fold can catch it. Corpus coverage 26/26 and 12/12 in all three languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,218 @@
|
|||||||
|
package attesto
|
||||||
|
|
||||||
|
// Go parity on offline disclosure verification.
|
||||||
|
//
|
||||||
|
// The fixture is a presentation the Local Vault really built through the real
|
||||||
|
// edge core. Three implementations agreeing with each other proves less than
|
||||||
|
// three agreeing with the producer, which is why it is not written to satisfy
|
||||||
|
// the verifiers.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadPresentation(t *testing.T) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(
|
||||||
|
"..", "..", "golden-vectors", "provenance-v0.1-dev",
|
||||||
|
"provenance-disclosure-presentation-valid.json",
|
||||||
|
)
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read fixture: %v", err)
|
||||||
|
}
|
||||||
|
var vector map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &vector); err != nil {
|
||||||
|
t.Fatalf("parse fixture: %v", err)
|
||||||
|
}
|
||||||
|
return vector
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentationCopy(t *testing.T, vector map[string]any) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
raw, err := json.Marshal(vector["presentation"])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("copy: %v", err)
|
||||||
|
}
|
||||||
|
var copied map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &copied); err != nil {
|
||||||
|
t.Fatalf("copy: %v", err)
|
||||||
|
}
|
||||||
|
return copied
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestARealDisclosureVerifiesAcrossLanguages(t *testing.T) {
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
report := VerifyDisclosure(
|
||||||
|
vector["presentation"].(map[string]any),
|
||||||
|
WithExpectedNonce(vector["nonce"].(string)),
|
||||||
|
)
|
||||||
|
if !report.Ok {
|
||||||
|
t.Fatalf("a real disclosure did not verify: %v", report.Problems)
|
||||||
|
}
|
||||||
|
expected := vector["expected"].(map[string]any)
|
||||||
|
if float64(len(report.VerifiedLeaves)) != expected["verified_leaf_count"].(float64) {
|
||||||
|
t.Fatalf("verified %d leaves, expected %v", len(report.VerifiedLeaves), expected["verified_leaf_count"])
|
||||||
|
}
|
||||||
|
if report.Freshness != "challenge" {
|
||||||
|
t.Fatalf("freshness: got %q want challenge", report.Freshness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithoutAChallengeTheReportSaysSo(t *testing.T) {
|
||||||
|
// Weaker evidence, reported as weaker rather than presented as the same.
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
report := VerifyDisclosure(vector["presentation"].(map[string]any))
|
||||||
|
if !report.Ok {
|
||||||
|
t.Fatalf("expiry-bounded verification failed: %v", report.Problems)
|
||||||
|
}
|
||||||
|
if report.Freshness != "bounded_lifetime" {
|
||||||
|
t.Fatalf("freshness: got %q want bounded_lifetime", report.Freshness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestASwappedValueDoesNotOpenItsLeaf(t *testing.T) {
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
tampered := presentationCopy(t, vector)
|
||||||
|
revealed := tampered["revealed"].([]any)
|
||||||
|
entry := revealed[0].(map[string]any)
|
||||||
|
entry["value"].(map[string]any)["value"] = map[string]any{"manifest_count": "9"}
|
||||||
|
|
||||||
|
report := VerifyDisclosure(tampered, WithExpectedNonce(vector["nonce"].(string)))
|
||||||
|
if report.Ok {
|
||||||
|
t.Fatal("a swapped value still verified")
|
||||||
|
}
|
||||||
|
if !hasDisclosureProblem(report.Problems, "does not open") {
|
||||||
|
t.Fatalf("expected an opening failure, got %v", report.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestALeafThatDoesNotFoldToTheRootIsRefused(t *testing.T) {
|
||||||
|
// A leaf can open its own commitment perfectly and still belong to a
|
||||||
|
// different capsule. Corrupting a sibling leaves the value and randomizer
|
||||||
|
// untouched, so only the two-hop fold can catch it — which is what
|
||||||
|
// distinguishes a verifier from a value checker.
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
tampered := presentationCopy(t, vector)
|
||||||
|
proofs := tampered["inclusion_proofs"].([]any)
|
||||||
|
steps := proofs[0].(map[string]any)["subtree_steps"].([]any)
|
||||||
|
steps[0].(map[string]any)["sibling"] = strings.Repeat("0", 64)
|
||||||
|
|
||||||
|
report := VerifyDisclosure(tampered, WithExpectedNonce(vector["nonce"].(string)))
|
||||||
|
if report.Ok || !hasDisclosureProblem(report.Problems, "not included") {
|
||||||
|
t.Fatalf("a leaf outside the capsule was accepted: %v", report.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACorruptedTopPathIsRefused(t *testing.T) {
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
tampered := presentationCopy(t, vector)
|
||||||
|
proofs := tampered["inclusion_proofs"].([]any)
|
||||||
|
steps := proofs[0].(map[string]any)["top_steps"].([]any)
|
||||||
|
steps[0].(map[string]any)["sibling"] = strings.Repeat("0", 64)
|
||||||
|
|
||||||
|
report := VerifyDisclosure(tampered, WithExpectedNonce(vector["nonce"].(string)))
|
||||||
|
if report.Ok || !hasDisclosureProblem(report.Problems, "not included") {
|
||||||
|
t.Fatalf("a corrupted top path was accepted: %v", report.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAReplayedNonceIsRefused(t *testing.T) {
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
report := VerifyDisclosure(
|
||||||
|
vector["presentation"].(map[string]any),
|
||||||
|
WithExpectedNonce(strings.Repeat("ff", 32)),
|
||||||
|
)
|
||||||
|
if report.Ok || !hasDisclosureProblem(report.Problems, "nonce") {
|
||||||
|
t.Fatalf("a replayed nonce was accepted: %v", report.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnExpiredDisclosureIsRefused(t *testing.T) {
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
report := VerifyDisclosure(
|
||||||
|
vector["presentation"].(map[string]any),
|
||||||
|
WithVerificationTime(time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
|
)
|
||||||
|
if report.Ok || !hasDisclosureProblem(report.Problems, "expired") {
|
||||||
|
t.Fatalf("an expired disclosure was accepted: %v", report.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestADisclosureForAnotherAssetIsRefused(t *testing.T) {
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
report := VerifyDisclosure(
|
||||||
|
vector["presentation"].(map[string]any),
|
||||||
|
WithSubjectCommitment(strings.Repeat("aa", 32)),
|
||||||
|
)
|
||||||
|
if report.Ok || report.SubjectChecked {
|
||||||
|
t.Fatalf("a foreign subject was accepted: %v", report.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTheMatchingSubjectIsReportedAsChecked(t *testing.T) {
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
report := VerifyDisclosure(
|
||||||
|
vector["presentation"].(map[string]any),
|
||||||
|
WithSubjectCommitment(vector["subject_commitment"].(string)),
|
||||||
|
)
|
||||||
|
if !report.Ok || !report.SubjectChecked {
|
||||||
|
t.Fatalf("the matching subject was not reported: %v", report.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestATamperedSignatureIsCaught(t *testing.T) {
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
tampered := presentationCopy(t, vector)
|
||||||
|
tampered["nonce"] = strings.Repeat("cd", 32)
|
||||||
|
report := VerifyDisclosure(tampered)
|
||||||
|
if report.Ok || !hasDisclosureProblem(report.Problems, "signature") {
|
||||||
|
t.Fatalf("a tampered presentation was accepted: %v", report.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEveryProblemIsCollected(t *testing.T) {
|
||||||
|
// A caller should see everything wrong with a presentation at once.
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
tampered := presentationCopy(t, vector)
|
||||||
|
revealed := tampered["revealed"].([]any)
|
||||||
|
revealed[0].(map[string]any)["value"].(map[string]any)["value"] = map[string]any{"x": "y"}
|
||||||
|
|
||||||
|
report := VerifyDisclosure(
|
||||||
|
tampered,
|
||||||
|
WithExpectedNonce(strings.Repeat("ff", 32)),
|
||||||
|
WithVerificationTime(time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
|
)
|
||||||
|
if len(report.Problems) < 3 {
|
||||||
|
t.Fatalf("expected several problems, got %v", report.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTheReportSaysWhatItDoesNotClaim(t *testing.T) {
|
||||||
|
vector := loadPresentation(t)
|
||||||
|
report := VerifyDisclosure(vector["presentation"].(map[string]any))
|
||||||
|
if len(report.NotClaimed) != 1 || report.NotClaimed[0]["id"] != "undisclosed_facts_absent" {
|
||||||
|
t.Fatalf("the non-claim is missing: %v", report.NotClaimed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnUnknownProtocolIsRefusedFirst(t *testing.T) {
|
||||||
|
report := VerifyDisclosure(map[string]any{"protocol": "SOMETHING-ELSE"})
|
||||||
|
if report.Ok || len(report.Problems) != 1 {
|
||||||
|
t.Fatalf("expected a single protocol refusal, got %v", report.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasDisclosureProblem(problems []string, needle string) bool {
|
||||||
|
for _, problem := range problems {
|
||||||
|
if strings.Contains(problem, needle) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
+273
@@ -14,11 +14,14 @@ package attesto
|
|||||||
// Conformance is defined by golden-vectors/provenance-v0.1-dev/.
|
// Conformance is defined by golden-vectors/provenance-v0.1-dev/.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -661,3 +664,273 @@ func ValidateRangeStatement(statement map[string]any) (uint, error) {
|
|||||||
}
|
}
|
||||||
return ZKRangeWidth(lower, upper)
|
return ZKRangeWidth(lower, upper)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------- disclosure verification
|
||||||
|
|
||||||
|
const (
|
||||||
|
DisclosureProtocol = "ATTESTO-DISCLOSURE-001"
|
||||||
|
DisclosureProtocolVersion = "0.1"
|
||||||
|
disclosureDomain = "attesto.provenance.v1.disclosure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// subtreeLeafDomain is the domain a leaf's own commitment was made under, as
|
||||||
|
// opposed to the domain its subtree folds in. Verifying a disclosure needs both:
|
||||||
|
// one opens the leaf, the other proves it belongs to the tree.
|
||||||
|
var subtreeLeafDomain = map[string]string{
|
||||||
|
"claims": "attesto.provenance.v1.claim",
|
||||||
|
"evidence": "attesto.provenance.v1.evidence",
|
||||||
|
"policy_results": "attesto.provenance.v1.policy_result",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisclosureNotClaimed is the seventh non-claim, on top of TM-05's six. A
|
||||||
|
// verifier that reported only what it checked would leave a reader to assume the
|
||||||
|
// picture is complete.
|
||||||
|
var DisclosureNotClaimed = map[string]string{
|
||||||
|
"id": "undisclosed_facts_absent",
|
||||||
|
"statement": "This disclosure proves the revealed leaves are in the capsule. " +
|
||||||
|
"It is not a statement that the capsule holds nothing else.",
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifiedLeaf is one leaf a presentation proved.
|
||||||
|
type VerifiedLeaf struct {
|
||||||
|
Subtree string `json:"subtree"`
|
||||||
|
LeafRole string `json:"leaf_role"`
|
||||||
|
Value any `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisclosureReport is what a presentation established, and what it did not.
|
||||||
|
//
|
||||||
|
// Ok is true only when every revealed leaf opened its commitment and every proof
|
||||||
|
// folded to the presented capsule root. Freshness and SubjectChecked are reported
|
||||||
|
// separately rather than folded in: a caller that issued no challenge got weaker
|
||||||
|
// evidence than one that did, and saying so is what separates a verifier from a
|
||||||
|
// rubber stamp.
|
||||||
|
type DisclosureReport struct {
|
||||||
|
Ok bool
|
||||||
|
CapsuleRoot string
|
||||||
|
VerifiedLeaves []VerifiedLeaf
|
||||||
|
Freshness string
|
||||||
|
SubjectChecked bool
|
||||||
|
Problems []string
|
||||||
|
NotClaimed []map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type disclosureOptions struct {
|
||||||
|
expectedNonce *string
|
||||||
|
subjectCommitment *string
|
||||||
|
now *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisclosureOption configures a verification.
|
||||||
|
type DisclosureOption func(*disclosureOptions)
|
||||||
|
|
||||||
|
// WithExpectedNonce turns on interactive mode: supply the challenge issued to
|
||||||
|
// the holder. Without it the presentation is only bounded by its expiry, which
|
||||||
|
// is weaker evidence, and the report says so.
|
||||||
|
func WithExpectedNonce(nonce string) DisclosureOption {
|
||||||
|
return func(o *disclosureOptions) { o.expectedNonce = &nonce }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSubjectCommitment checks the presentation is bound to the asset held.
|
||||||
|
func WithSubjectCommitment(commitment string) DisclosureOption {
|
||||||
|
return func(o *disclosureOptions) { o.subjectCommitment = &commitment }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithVerificationTime overrides the clock, for testing expiry without waiting.
|
||||||
|
func WithVerificationTime(at time.Time) DisclosureOption {
|
||||||
|
return func(o *disclosureOptions) { o.now = &at }
|
||||||
|
}
|
||||||
|
|
||||||
|
func disclosureSigningPayload(presentation map[string]any) map[string]any {
|
||||||
|
payload := make(map[string]any, len(presentation))
|
||||||
|
for key, value := range presentation {
|
||||||
|
if key != "signature" {
|
||||||
|
payload[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyDisclosure verifies a selective disclosure offline.
|
||||||
|
//
|
||||||
|
// It needs no network and no platform: the presentation carries its own
|
||||||
|
// signature, the revealed values with their randomizers, and two-hop proofs to
|
||||||
|
// the capsule root.
|
||||||
|
//
|
||||||
|
// Every problem is collected rather than returned on the first one, so a caller
|
||||||
|
// sees all of what is wrong with a presentation instead of only the earliest.
|
||||||
|
func VerifyDisclosure(presentation map[string]any, options ...DisclosureOption) DisclosureReport {
|
||||||
|
opts := &disclosureOptions{}
|
||||||
|
for _, option := range options {
|
||||||
|
option(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
notClaimed := []map[string]string{DisclosureNotClaimed}
|
||||||
|
if presentation["protocol"] != DisclosureProtocol ||
|
||||||
|
presentation["protocol_version"] != DisclosureProtocolVersion {
|
||||||
|
return DisclosureReport{
|
||||||
|
Freshness: "unknown",
|
||||||
|
Problems: []string{"unsupported disclosure protocol"},
|
||||||
|
NotClaimed: notClaimed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
problems := []string{}
|
||||||
|
moment := time.Now().UTC()
|
||||||
|
if opts.now != nil {
|
||||||
|
moment = *opts.now
|
||||||
|
}
|
||||||
|
if raw, ok := presentation["expires_at"].(string); ok {
|
||||||
|
if expires, err := time.Parse(time.RFC3339Nano, raw); err != nil {
|
||||||
|
problems = append(problems, "expiry is malformed")
|
||||||
|
} else if !moment.Before(expires) {
|
||||||
|
problems = append(problems, "disclosure has expired")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
problems = append(problems, "expiry is malformed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.expectedNonce != nil && presentation["nonce"] != *opts.expectedNonce {
|
||||||
|
problems = append(problems, "nonce is not the one issued")
|
||||||
|
}
|
||||||
|
|
||||||
|
problems = append(problems, verifyDisclosureSignature(presentation)...)
|
||||||
|
|
||||||
|
revealed := map[string]map[string]any{}
|
||||||
|
for _, raw := range asSlice(presentation["revealed"]) {
|
||||||
|
if entry, ok := raw.(map[string]any); ok {
|
||||||
|
revealed[toString(entry["leaf_id"])] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proofs := map[string]map[string]any{}
|
||||||
|
for _, raw := range asSlice(presentation["inclusion_proofs"]) {
|
||||||
|
if proof, ok := raw.(map[string]any); ok {
|
||||||
|
proofs[toString(proof["leaf_id"])] = proof
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(revealed) == 0 || len(revealed) != len(proofs) {
|
||||||
|
problems = append(problems, "every revealed leaf needs its proof and every proof its leaf")
|
||||||
|
}
|
||||||
|
|
||||||
|
capsuleRoot := toString(presentation["capsule_root"])
|
||||||
|
verified := []VerifiedLeaf{}
|
||||||
|
for leafID, entry := range revealed {
|
||||||
|
proof, ok := proofs[leafID]
|
||||||
|
if !ok {
|
||||||
|
problems = append(problems, "every revealed leaf needs its proof and every proof its leaf")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subtree := toString(entry["subtree"])
|
||||||
|
if toString(proof["capsule_root"]) != capsuleRoot || toString(proof["subtree"]) != subtree {
|
||||||
|
problems = append(problems, "proof does not match its revealed leaf: "+leafID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
domain, known := subtreeLeafDomain[subtree]
|
||||||
|
if !known {
|
||||||
|
problems = append(problems, "unknown subtree: "+subtree)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opened, err := VerifyProvenanceCommitment(
|
||||||
|
domain, entry["value"], toString(entry["randomizer"]), toString(proof["leaf"]),
|
||||||
|
)
|
||||||
|
if err != nil || !opened {
|
||||||
|
problems = append(problems, "revealed value does not open its leaf commitment: "+leafID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
included, err := verifyTwoHopFromMap(proof)
|
||||||
|
if err != nil || !included {
|
||||||
|
problems = append(problems, "leaf is not included under the presented capsule root: "+leafID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
leafValue := entry["value"]
|
||||||
|
if wrapped, ok := leafValue.(map[string]any); ok {
|
||||||
|
leafValue = wrapped["value"]
|
||||||
|
}
|
||||||
|
verified = append(verified, VerifiedLeaf{
|
||||||
|
Subtree: subtree, LeafRole: toString(entry["leaf_role"]), Value: leafValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
subjectChecked := false
|
||||||
|
if opts.subjectCommitment != nil {
|
||||||
|
if toString(presentation["subject_binding"]) != *opts.subjectCommitment {
|
||||||
|
problems = append(problems, "disclosure is bound to a different asset")
|
||||||
|
} else {
|
||||||
|
subjectChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
freshness := "bounded_lifetime"
|
||||||
|
if opts.expectedNonce != nil {
|
||||||
|
freshness = "challenge"
|
||||||
|
}
|
||||||
|
return DisclosureReport{
|
||||||
|
Ok: len(problems) == 0,
|
||||||
|
CapsuleRoot: capsuleRoot,
|
||||||
|
VerifiedLeaves: verified,
|
||||||
|
Freshness: freshness,
|
||||||
|
SubjectChecked: subjectChecked,
|
||||||
|
Problems: problems,
|
||||||
|
NotClaimed: notClaimed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyDisclosureSignature(presentation map[string]any) []string {
|
||||||
|
signature, _ := presentation["signature"].(map[string]any)
|
||||||
|
issuer, _ := presentation["issuer"].(map[string]any)
|
||||||
|
if signature == nil || issuer == nil ||
|
||||||
|
toString(signature["algorithm"]) != "ed25519" ||
|
||||||
|
toString(signature["domain"]) != disclosureDomain {
|
||||||
|
return []string{"signature is not a v1 disclosure signature"}
|
||||||
|
}
|
||||||
|
publicKey, err := hex.DecodeString(toString(issuer["public_key"]))
|
||||||
|
if err != nil || len(publicKey) != ed25519.PublicKeySize {
|
||||||
|
return []string{"signature did not verify"}
|
||||||
|
}
|
||||||
|
sig, err := hex.DecodeString(toString(signature["value"]))
|
||||||
|
if err != nil || len(sig) != ed25519.SignatureSize {
|
||||||
|
return []string{"signature did not verify"}
|
||||||
|
}
|
||||||
|
payload, err := CanonicalJSON(disclosureSigningPayload(presentation))
|
||||||
|
if err != nil {
|
||||||
|
return []string{"signature did not verify"}
|
||||||
|
}
|
||||||
|
message := append([]byte(disclosureDomain), 0)
|
||||||
|
message = append(message, payload...)
|
||||||
|
if !ed25519.Verify(publicKey, message, sig) {
|
||||||
|
return []string{"signature did not verify"}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyTwoHopFromMap(proof map[string]any) (bool, error) {
|
||||||
|
scrubbed := make(map[string]any, len(proof))
|
||||||
|
for key, value := range proof {
|
||||||
|
if key != "leaf_id" {
|
||||||
|
scrubbed[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(scrubbed)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
var typed TwoHopProof
|
||||||
|
if err := json.Unmarshal(encoded, &typed); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return VerifyTwoHop(typed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func asSlice(value any) []any {
|
||||||
|
if typed, ok := value.([]any); ok {
|
||||||
|
return typed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toString(value any) string {
|
||||||
|
if typed, ok := value.(string); ok {
|
||||||
|
return typed
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user