Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions token/core/zkatdlog/nogh/v1/audit/auditor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ SPDX-License-Identifier: Apache-2.0
package audit

import (
"bytes"
"context"

math "github.com/IBM/mathlib"
Expand Down Expand Up @@ -122,7 +121,6 @@ func (a *Auditor) Check(
inputTokens [][]*token.Token,
txID driver.TokenRequestAnchor,
) error {
// TODO: inputTokens should be checked against the actions
// De-obfuscate issue requests
a.Logger.DebugfContext(ctx, "Get audit info for %d issues", len(tokenRequest.Issues))
outputsFromIssue, identitiesFromIssue, err := a.GetAuditInfoForIssues(tokenRequest.Issues, tokenRequestMetadata.Issues)
Expand All @@ -144,7 +142,7 @@ func (a *Auditor) Check(
}
// De-obfuscate transfer requests
a.Logger.DebugfContext(ctx, "Get audit info for %d transfers", len(tokenRequest.Transfers))
auditableInputs, outputsFromTransfer, err := a.GetAuditInfoForTransfers(tokenRequest.Transfers, tokenRequestMetadata.Transfers, inputTokens)
auditableInputs, outputsFromTransfer, err := a.GetAuditInfoForTransfers(ctx, tokenRequest.Transfers, tokenRequestMetadata.Transfers, inputTokens)
if err != nil {
return errors.Wrapf(err, "failed getting audit info for transfers for [%s]", txID)
}
Expand Down Expand Up @@ -258,7 +256,7 @@ func (a *Auditor) InspectIdentity(ctx context.Context, matcher InfoMatcher, iden
// If identity is provided in metadata, it must match the one in the action
if len(identity.IdentityFromMeta) != 0 {
// enforce equality
if !bytes.Equal(identity.IdentityFromMeta, identity.Identity) {
if !identity.IdentityFromMeta.Equal(identity.Identity) {
return errors.Errorf("failed to inspect identity at index [%d]: identity does not match the identity form metadata", index)
}
}
Expand Down Expand Up @@ -333,7 +331,7 @@ func (a *Auditor) GetAuditInfoForIssues(issues [][]byte, issueMetadata []*driver

// GetAuditInfoForTransfers returns an array of InspectableToken for each transfer action.
// It takes an array of serialized transfer actions, an array of transfer metadata and input tokens.
func (a *Auditor) GetAuditInfoForTransfers(transfers [][]byte, metadata []*driver.TransferMetadata, inputs [][]*token.Token) ([][]*InspectableToken, [][]*InspectableToken, error) {
func (a *Auditor) GetAuditInfoForTransfers(ctx context.Context, transfers [][]byte, metadata []*driver.TransferMetadata, inputs [][]*token.Token) ([][]*InspectableToken, [][]*InspectableToken, error) {
if len(transfers) != len(metadata) {
return nil, nil, errors.Errorf("number of transfers does not match the number of provided metadata")
}
Expand Down Expand Up @@ -363,29 +361,50 @@ func (a *Auditor) GetAuditInfoForTransfers(transfers [][]byte, metadata []*drive
}
}
ta := &transfer.Action{}
err := ta.Deserialize(transfers[k])
if err != nil {
if err := ta.Deserialize(transfers[k]); err != nil {
return nil, nil, err
}
if len(ta.Outputs) != len(transferMetadata.Outputs) {
return nil, nil, errors.Errorf("number of outputs does not match the number of output metadata [%d]!=[%d]", len(ta.Outputs), len(transferMetadata.Outputs))
// Validate structural consistency between action and metadata (counts, signers, issuer).
if err := transferMetadata.Match(ta); err != nil {
return nil, nil, errors.Wrapf(err, "transfer at index [%d]", k)
}
// Build serialized forms and validate input tokens match the action.
actionInputSer := make([][]byte, len(ta.Inputs))
for i, actionInput := range ta.Inputs {
if actionInput.Token == nil {
continue // upgrade-witness input: no zkatdlog commitment to compare
}
ser, err := actionInput.Token.Serialize()
if err != nil {
return nil, nil, errors.Errorf("failed serializing action input at [%d][%d]: %s", k, i, err)
}
actionInputSer[i] = ser
}
ledgerInputSer := make([][]byte, len(inputs[k]))
for i, t := range inputs[k] {
ser, err := t.Serialize()
if err != nil {
return nil, nil, errors.Errorf("failed serializing ledger input at [%d][%d]: %s", k, i, err)
}
ledgerInputSer[i] = ser
}
if err := transferMetadata.MatchInputs(actionInputSer, ledgerInputSer); err != nil {
return nil, nil, errors.Wrapf(err, "transfer at index [%d]", k)
}
// Process auditable outputs
outputs[k] = make([]*InspectableToken, len(ta.Outputs))
for i := range len(ta.Outputs) {
if ta.Outputs[i] == nil {
return nil, nil, errors.Errorf("output token at index [%d] is nil", i)
}

if transferMetadata.Outputs[i] == nil {
return nil, nil, errors.Errorf("metadata for output token at index [%d] is nil", i)
}
ti := &token.Metadata{}
err = ti.Deserialize(transferMetadata.Outputs[i].OutputMetadata)
err := ti.Deserialize(transferMetadata.Outputs[i].OutputMetadata)
if err != nil {
return nil, nil, err
}
// TODO: we need to check also how many recipients the output contains, and check them all in isolation and compatibility
outputs[k][i], err = NewInspectableToken(
ta.Outputs[i],
transferMetadata.Outputs[i].OutputAuditInfo,
Expand All @@ -396,6 +415,24 @@ func (a *Auditor) GetAuditInfoForTransfers(transfers [][]byte, metadata []*drive
if err != nil {
return nil, nil, err
}
// Verify each receiver's audit info matches their identity for non-redeem outputs.
if !ta.Outputs[i].IsRedeem() {
if err := transferMetadata.Outputs[i].ValidateReceivers(); err != nil {
return nil, nil, errors.Wrapf(err, "output at index [%d][%d]", k, i)
}
for j, receiver := range transferMetadata.Outputs[i].Receivers {
identityToVerify := receiver.Identity
if identityToVerify.IsNone() {
identityToVerify = ta.Outputs[i].Owner
}
if err := a.InspectIdentity(ctx, a.InfoMatcher, &InspectableIdentity{
Identity: identityToVerify,
AuditInfo: receiver.AuditInfo,
}, j); err != nil {
return nil, nil, errors.Wrapf(err, "failed inspecting receiver at index [%d][%d][%d]", k, i, j)
}
}
}
}
}

Expand Down
80 changes: 66 additions & 14 deletions token/core/zkatdlog/nogh/v1/audit/auditor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,11 @@ func TestAuditor_Errors(t *testing.T) {
// in the number of transfers, transfer metadata, or input tokens.
t.Run("GetAuditInfoForTransfers length mismatch", func(t *testing.T) {
_, _, auditor := setupAuditorTest(t)
_, _, err := auditor.GetAuditInfoForTransfers([][]byte{{1}}, nil, nil)
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{{1}}, nil, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "number of transfers does not match the number of provided metadata")

_, _, err = auditor.GetAuditInfoForTransfers([][]byte{{1}}, []*driver.TransferMetadata{{}}, nil)
_, _, err = auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{{1}}, []*driver.TransferMetadata{{}}, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "number of inputs does not match the number of provided metadata")
})
Expand Down Expand Up @@ -280,7 +280,7 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) {
// for a transfer is nil.
t.Run("GetAuditInfoForTransfers nil input token", func(t *testing.T) {
_, _, auditor := setupAuditorTest(t)
_, _, err := auditor.GetAuditInfoForTransfers([][]byte{{}}, []*driver.TransferMetadata{{Inputs: []*driver.TransferInputMetadata{{}}}}, [][]*token.Token{{nil}})
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{{}}, []*driver.TransferMetadata{{Inputs: []*driver.TransferInputMetadata{{}}}}, [][]*token.Token{{nil}})
require.Error(t, err)
require.Contains(t, err.Error(), "input[0][0] is nil")
})
Expand All @@ -289,7 +289,7 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) {
// metadata for a transfer input is nil.
t.Run("GetAuditInfoForTransfers invalid input metadata", func(t *testing.T) {
_, _, auditor := setupAuditorTest(t)
_, _, err := auditor.GetAuditInfoForTransfers([][]byte{{}}, []*driver.TransferMetadata{{Inputs: []*driver.TransferInputMetadata{nil}}}, [][]*token.Token{{&token.Token{}}})
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{{}}, []*driver.TransferMetadata{{Inputs: []*driver.TransferInputMetadata{nil}}}, [][]*token.Token{{&token.Token{}}})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid metadata for input[0][0]")
})
Expand All @@ -299,7 +299,7 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) {
t.Run("GetAuditInfoForTransfers transfer deserialization error", func(t *testing.T) {
_, _, auditor := setupAuditorTest(t)
inputs := []*driver.TransferInputMetadata{{Senders: []*driver.AuditableIdentity{{AuditInfo: []byte{1}}}}}
_, _, err := auditor.GetAuditInfoForTransfers([][]byte{{1, 2, 3}}, []*driver.TransferMetadata{{Inputs: inputs}}, [][]*token.Token{{&token.Token{}}})
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{{1, 2, 3}}, []*driver.TransferMetadata{{Inputs: inputs}}, [][]*token.Token{{&token.Token{}}})
require.Error(t, err)
require.Contains(t, err.Error(), "failed to deserialize transfer action")
})
Expand All @@ -308,22 +308,22 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) {
// of outputs in a transfer action does not match the number of provided output metadata.
t.Run("GetAuditInfoForTransfers output count mismatch", func(t *testing.T) {
_, pp, auditor := setupAuditorTest(t)
transfer, meta, _ := createTransfer(t, pp)
transfer, meta, tokens := createTransfer(t, pp)
raw, _ := transfer.Serialize()
meta.Outputs = meta.Outputs[:len(meta.Outputs)-1]
_, _, err := auditor.GetAuditInfoForTransfers([][]byte{raw}, []*driver.TransferMetadata{meta}, [][]*token.Token{{&token.Token{}, &token.Token{}}})
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens)
require.Error(t, err)
require.Contains(t, err.Error(), "number of outputs does not match the number of output metadata")
require.Contains(t, err.Error(), "expected [1] outputs but got [2]")
})

// GetAuditInfoForTransfers nil output token tests that an error is returned when one of the outputs
// in a transfer action is nil.
t.Run("GetAuditInfoForTransfers nil output token", func(t *testing.T) {
_, pp, auditor := setupAuditorTest(t)
transfer, meta, _ := createTransfer(t, pp)
transfer, meta, tokens := createTransfer(t, pp)
transfer.Outputs[0] = nil
raw, _ := transfer.Serialize()
_, _, err := auditor.GetAuditInfoForTransfers([][]byte{raw}, []*driver.TransferMetadata{meta}, [][]*token.Token{{&token.Token{}, &token.Token{}}})
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens)
require.Error(t, err)
require.Contains(t, err.Error(), "output token at index [0] is nil")
})
Expand All @@ -332,10 +332,10 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) {
// for a transfer output is nil.
t.Run("GetAuditInfoForTransfers nil output metadata", func(t *testing.T) {
_, pp, auditor := setupAuditorTest(t)
transfer, meta, _ := createTransfer(t, pp)
transfer, meta, tokens := createTransfer(t, pp)
meta.Outputs[0] = nil
raw, _ := transfer.Serialize()
_, _, err := auditor.GetAuditInfoForTransfers([][]byte{raw}, []*driver.TransferMetadata{meta}, [][]*token.Token{{&token.Token{}, &token.Token{}}})
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens)
require.Error(t, err)
require.Contains(t, err.Error(), "metadata for output token at index [0] is nil")
})
Expand All @@ -344,13 +344,65 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) {
// the metadata for a transfer output cannot be deserialized.
t.Run("GetAuditInfoForTransfers output metadata deserialization error", func(t *testing.T) {
_, pp, auditor := setupAuditorTest(t)
transfer, meta, _ := createTransfer(t, pp)
transfer, meta, tokens := createTransfer(t, pp)
meta.Outputs[0].OutputMetadata = []byte{1, 2, 3}
raw, _ := transfer.Serialize()
_, _, err := auditor.GetAuditInfoForTransfers([][]byte{raw}, []*driver.TransferMetadata{meta}, [][]*token.Token{{&token.Token{}, &token.Token{}}})
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens)
require.Error(t, err)
require.Contains(t, err.Error(), "failed deserializing metadata")
})

// GetAuditInfoForTransfers input token commitment mismatch tests that an error is returned when
// an input token's serialized form does not match the one embedded in the transfer action.
t.Run("GetAuditInfoForTransfers input token commitment mismatch", func(t *testing.T) {
_, pp, auditor := setupAuditorTest(t)
transfer, meta, tokens := createTransfer(t, pp)
raw, _ := transfer.Serialize()
tokens[0][0].Data = pp.PedersenGenerators[0] // tamper with commitment
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens)
require.Error(t, err)
require.Contains(t, err.Error(), "does not match the transfer action")
})

// GetAuditInfoForTransfers input token owner mismatch tests that an error is returned when
// an input token's serialized form does not match the one embedded in the transfer action.
t.Run("GetAuditInfoForTransfers input token owner mismatch", func(t *testing.T) {
_, pp, auditor := setupAuditorTest(t)
transfer, meta, tokens := createTransfer(t, pp)
raw, _ := transfer.Serialize()
tokens[0][0].Owner = []byte("wrong-owner") // tamper with owner
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens)
require.Error(t, err)
require.Contains(t, err.Error(), "does not match the transfer action")
})

// GetAuditInfoForTransfers no receivers for output tests that an error is returned when a
// non-redeemed output has no declared receivers.
t.Run("GetAuditInfoForTransfers no receivers for output", func(t *testing.T) {
_, pp, auditor := setupAuditorTest(t)
transfer, meta, tokens := createTransfer(t, pp)
raw, _ := transfer.Serialize()
meta.Outputs[0].Receivers = nil
_, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens)
require.Error(t, err)
require.Contains(t, err.Error(), "has no receivers")
})

// GetAuditInfoForTransfers receiver audit info mismatch tests that an error is returned when
// a receiver's audit info does not match the output owner.
t.Run("GetAuditInfoForTransfers receiver audit info mismatch", func(t *testing.T) {
_, pp, auditor := setupAuditorTest(t)
transfer, meta, tokens := createTransfer(t, pp)
_, differentAuditInfo := getIdemixInfo(t, "./testdata/bls12_381_bbs/idemix")
differentAuditInfoRaw, err := differentAuditInfo.Bytes()
require.NoError(t, err)
raw, err := transfer.Serialize()
require.NoError(t, err)
meta.Outputs[0].Receivers[0].AuditInfo = differentAuditInfoRaw
_, _, err = auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens)
require.Error(t, err)
require.Contains(t, err.Error(), "failed inspecting receiver")
})
}

// TestAuditor_Check_Errors tests error handling for the Check method, ensuring the auditor
Expand Down
96 changes: 96 additions & 0 deletions token/driver/match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package driver

import (
"bytes"
"slices"

"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
)

// Match validates the structural consistency between this metadata and the provided issue action.
// It checks input/output counts, extra signers, and the issuer identity.
func (i *IssueMetadata) Match(action IssueAction) error {
if len(i.Inputs) != action.NumInputs() {
return errors.Errorf("expected [%d] inputs but got [%d]", len(i.Inputs), action.NumInputs())
}
if len(i.Outputs) != action.NumOutputs() {
return errors.Errorf("expected [%d] outputs but got [%d]", len(i.Outputs), action.NumOutputs())
}
extraSigners := action.ExtraSigners()
if len(i.ExtraSigners) != len(extraSigners) {
return errors.Errorf("expected [%d] extra signers but got [%d]", len(extraSigners), len(i.ExtraSigners))
}
for idx, signer := range extraSigners {
if !slices.ContainsFunc(i.ExtraSigners, signer.Equal) {
return errors.Errorf("expected extra signer [%s] but got [%s]", signer, i.ExtraSigners[idx])
}
}
if !i.Issuer.Identity.Equal(action.GetIssuer()) {
return errors.Errorf("expected issuer [%s] but got [%s]", i.Issuer.Identity, action.GetIssuer())
}

return nil
}

// Match validates the structural consistency between this metadata and the provided transfer action.
// It checks input/output counts, extra signers, and the issuer identity.
func (t *TransferMetadata) Match(action TransferAction) error {
if len(t.Inputs) != action.NumInputs() {
return errors.Errorf("expected [%d] inputs but got [%d]", len(t.Inputs), action.NumInputs())
}
if len(t.Outputs) != action.NumOutputs() {
return errors.Errorf("expected [%d] outputs but got [%d]", len(t.Outputs), action.NumOutputs())
}
extraSigners := action.ExtraSigners()
if len(t.ExtraSigners) != len(extraSigners) {
return errors.Errorf("expected [%d] extra signers but got [%d]", len(t.ExtraSigners), len(extraSigners))
}
for idx, signer := range extraSigners {
if !signer.Equal(t.ExtraSigners[idx]) {
return errors.Errorf("expected extra signer [%s] but got [%s]", t.ExtraSigners[idx], signer)
}
}
if !t.Issuer.Equal(action.GetIssuer()) {
return errors.Errorf("expected issuer [%s] but got [%s]", t.Issuer, action.GetIssuer().Bytes())
}

return nil
}

// MatchInputs validates that the serialized action inputs match the serialized ledger tokens.
// Nil entries in serializedActionInputs are skipped (e.g., upgrade-witness inputs).
func (t *TransferMetadata) MatchInputs(serializedActionInputs [][]byte, serializedLedgerTokens [][]byte) error {
if len(serializedActionInputs) != len(serializedLedgerTokens) {
return errors.Errorf("action has [%d] inputs but [%d] tokens provided", len(serializedActionInputs), len(serializedLedgerTokens))
}
for i, actionInput := range serializedActionInputs {
if actionInput == nil {
continue
}
if !bytes.Equal(actionInput, serializedLedgerTokens[i]) {
return errors.Errorf("input token at index [%d]: does not match the transfer action", i)
}
}

return nil
}

// ValidateReceivers checks that this output has at least one non-nil receiver declared.
func (t *TransferOutputMetadata) ValidateReceivers() error {
if len(t.Receivers) == 0 {
return errors.New("has no receivers")
}
for j, receiver := range t.Receivers {
if receiver == nil {
return errors.Errorf("receiver at index [%d] is nil", j)
}
}

return nil
}
Loading
Loading