Skip to content

Commit 5db7643

Browse files
committed
fix(identity): derive the enrollment ID of a boolpolicy identity from its members
A policy identity's AuditInfo always reported an empty enrollment ID, so policy-owned inputs and outputs reach the auditor with no enrollment attribution and downstream eid-keyed accounting misbooks them. Resolve each component's audit info through the parent multiplex deserializer (same recursive pattern as htlc.NewAuditDeserializer) and report the enrollment ID shared by all components. A component with no enrollment ID of its own (e.g. a nested composite spanning enrollments) or disagreeing components yield the legacy empty value. Malformed audit info -- an unresolvable component or a component count mismatch -- is an error rather than a silent empty value, and every component is resolved before the result is declared so later corruption is never masked. Add a fuzz target for the new recursive deserialization path and wire it into the nightly fuzz workflow. Signed-off-by: Evan <evanyan@sign.global>
1 parent 1e74ae8 commit 5db7643

8 files changed

Lines changed: 369 additions & 6 deletions

File tree

.github/workflows/nightly-fuzz.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ jobs:
6060
- name: identity-idemixnym-audit-info-deserializer
6161
pkg: ./token/services/identity/idemixnym/nym
6262
func: FuzzDeserializeAuditInfoNoPanic
63+
- name: identity-boolpolicy-audit-info-deserializer
64+
pkg: ./token/services/identity/boolpolicy
65+
func: FuzzDeserializeAuditInfoNoPanic
6366
- name: common-request-limits
6467
pkg: ./token/core/common
6568
func: FuzzRequestResourceLimits

docs/services/identity.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ Located in `token/services/identity/boolpolicy`.
325325
- `identities` (SEQUENCE OF OCTET STRING): ordered list of raw component identity bytes; `$N` indexes into this list.
326326
* **Audit Info**: JSON-encoded `AuditInfo` structure.
327327
- `IdentityAuditInfos` (array of `IdentityAuditInfo`): per-component audit info blobs in the same order as `identities`.
328+
* **Enrollment ID**: When the audit-info deserializer is built with the parent multiplex deserializer (`NewAuditInfoDeserializer`), the policy identity reports the enrollment ID shared by all component identities. Components with no enrollment ID of their own (e.g. a nested composite spanning enrollments) or disagreeing components yield an empty enrollment ID; an unresolvable component or a component count mismatch is an error.
328329
* **Encoding**:
329330
- `TypedIdentity` payload: ASN.1 DER.
330331
- Audit Info: JSON.

token/core/fabtoken/v1/driver/deserializer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func NewEIDRHDeserializer() *EIDRHDeserializer {
5252
d.AddDeserializer(x509.IdentityType, &x509.AuditInfoDeserializer{})
5353
d.AddDeserializer(htlc2.ScriptType, htlc.NewAuditDeserializer(d))
5454
d.AddDeserializer(multisig.Multisig, &multisig.AuditInfoDeserializer{})
55-
d.AddDeserializer(boolpolicy.Policy, &boolpolicy.AuditInfoDeserializer{})
55+
d.AddDeserializer(boolpolicy.Policy, boolpolicy.NewAuditInfoDeserializer(d))
5656

5757
return d
5858
}

token/core/zkatdlog/nogh/v1/driver/deserializer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func NewEIDRHDeserializer() *EIDRHDeserializer {
9494
d.AddDeserializer(x509.IdentityType, &x509.AuditInfoDeserializer{})
9595
d.AddDeserializer(htlc2.ScriptType, htlc.NewAuditDeserializer(d))
9696
d.AddDeserializer(multisig.Multisig, &multisig.AuditInfoDeserializer{})
97-
d.AddDeserializer(boolpolicy.Policy, &boolpolicy.AuditInfoDeserializer{})
97+
d.AddDeserializer(boolpolicy.Policy, boolpolicy.NewAuditInfoDeserializer(d))
9898

9999
return d
100100
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package boolpolicy
8+
9+
import (
10+
"testing"
11+
12+
"github.com/LFDT-Panurus/panurus/token/services/identity"
13+
"github.com/LFDT-Panurus/panurus/token/services/identity/x509"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
const maxFuzzAuditInfoBytes = 64 << 10
18+
19+
// fuzzMember returns a typed x509 member identity and its audit info bytes.
20+
func fuzzMember(f *testing.F, name, eid string) ([]byte, []byte) {
21+
f.Helper()
22+
member, err := identity.WrapWithType(x509.IdentityType, []byte(name))
23+
require.NoError(f, err)
24+
auditInfo, err := (&x509.AuditInfo{EID: eid, RH: []byte("rh-" + eid)}).Bytes()
25+
require.NoError(f, err)
26+
27+
return member, auditInfo
28+
}
29+
30+
// fuzzPolicy wraps members into a policy identity (inner DER and typed
31+
// envelope) and the matching composite audit info blob.
32+
func fuzzPolicy(f *testing.F, policy string, members, auditInfos [][]byte) (inner, envelope, wrapped []byte) {
33+
f.Helper()
34+
inner, err := (&PolicyIdentity{Policy: policy, Identities: members}).Serialize()
35+
require.NoError(f, err)
36+
envelope, err = identity.WrapWithType(Policy, inner)
37+
require.NoError(f, err)
38+
wrapped, err = WrapAuditInfo(auditInfos)
39+
require.NoError(f, err)
40+
41+
return inner, envelope, wrapped
42+
}
43+
44+
func FuzzDeserializeAuditInfoNoPanic(f *testing.F) {
45+
m0, ai0 := fuzzMember(f, "cert-zero", "wallet-42")
46+
m1, ai1 := fuzzMember(f, "cert-one", "wallet-43")
47+
48+
// valid single- and two-member policies, both identity encodings
49+
inner1, envelope1, wrapped1 := fuzzPolicy(f, "$0", [][]byte{m0}, [][]byte{ai0})
50+
f.Add(inner1, wrapped1)
51+
f.Add(envelope1, wrapped1)
52+
inner2, _, wrapped2 := fuzzPolicy(f, "$0 OR $1", [][]byte{m0, m1}, [][]byte{ai0, ai1})
53+
f.Add(inner2, wrapped2)
54+
// empty and truncated inputs
55+
f.Add([]byte{}, []byte{})
56+
f.Add(inner2[:len(inner2)/2], wrapped2[:len(wrapped2)/2])
57+
f.Add(inner2, []byte(`{"IdentityAuditInfos":`))
58+
f.Add(inner1, []byte(`{}`))
59+
// member count mismatch
60+
innerMismatch, _, wrappedOne := fuzzPolicy(f, "$0 OR $1", [][]byte{m0, m1}, [][]byte{ai0})
61+
f.Add(innerMismatch, wrappedOne)
62+
// unknown member identity type
63+
unknown, err := identity.WrapWithType(identity.Type(99), []byte("cert-x"))
64+
require.NoError(f, err)
65+
innerUnknown, _, wrappedUnknown := fuzzPolicy(f, "$0", [][]byte{unknown}, [][]byte{ai0})
66+
f.Add(innerUnknown, wrappedUnknown)
67+
// nested and deeply nested policies
68+
nestedID, nestedInfo := m0, ai0
69+
for range 5 {
70+
_, nestedID, nestedInfo = fuzzPolicy(f, "$0", [][]byte{nestedID}, [][]byte{nestedInfo})
71+
}
72+
innerDeep, _, wrappedDeep := fuzzPolicy(f, "$0", [][]byte{nestedID}, [][]byte{nestedInfo})
73+
f.Add(innerDeep, wrappedDeep)
74+
75+
f.Fuzz(func(t *testing.T, rawID, rawInfo []byte) {
76+
if len(rawID) > maxFuzzAuditInfoBytes || len(rawInfo) > maxFuzzAuditInfoBytes {
77+
t.Skip()
78+
}
79+
require.NotPanics(t, func() {
80+
_, _ = NewAuditInfoDeserializer(newEIDRHDeserializer()).DeserializeAuditInfo(t.Context(), rawID, rawInfo)
81+
})
82+
})
83+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package boolpolicy
8+
9+
import (
10+
"testing"
11+
12+
"github.com/LFDT-Panurus/panurus/token"
13+
"github.com/LFDT-Panurus/panurus/token/services/identity"
14+
"github.com/LFDT-Panurus/panurus/token/services/identity/deserializer"
15+
"github.com/LFDT-Panurus/panurus/token/services/identity/x509"
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
// newX509Member returns a typed x509 member identity and its audit info bytes.
21+
func newX509Member(t *testing.T, name, eid string) ([]byte, []byte) {
22+
t.Helper()
23+
member, err := identity.WrapWithType(x509.IdentityType, []byte(name))
24+
require.NoError(t, err)
25+
auditInfo, err := (&x509.AuditInfo{EID: eid, RH: []byte("rh-" + eid)}).Bytes()
26+
require.NoError(t, err)
27+
28+
return member, auditInfo
29+
}
30+
31+
// newPolicyIdentity wraps the members into a typed policy identity and the
32+
// matching composite audit info blob.
33+
func newPolicyIdentity(t *testing.T, policy string, members [][]byte, auditInfos [][]byte) (token.Identity, []byte) {
34+
t.Helper()
35+
inner, err := (&PolicyIdentity{Policy: policy, Identities: members}).Serialize()
36+
require.NoError(t, err)
37+
policyID, err := identity.WrapWithType(Policy, inner)
38+
require.NoError(t, err)
39+
wrapped, err := WrapAuditInfo(auditInfos)
40+
require.NoError(t, err)
41+
42+
return policyID, wrapped
43+
}
44+
45+
// newEIDRHDeserializer mirrors the driver wiring: x509 plus a recursive
46+
// boolpolicy audit-info deserializer.
47+
func newEIDRHDeserializer() *deserializer.EIDRHDeserializer {
48+
d := deserializer.NewEIDRHDeserializer()
49+
d.AddDeserializer(x509.IdentityType, &x509.AuditInfoDeserializer{})
50+
d.AddDeserializer(Policy, NewAuditInfoDeserializer(d))
51+
52+
return d
53+
}
54+
55+
func TestPolicyEnrollmentIDCommonMembers(t *testing.T) {
56+
m0, ai0 := newX509Member(t, "cert-zero", "wallet-42")
57+
m1, ai1 := newX509Member(t, "cert-one", "wallet-42")
58+
policyID, wrapped := newPolicyIdentity(t, "$0 OR $1", [][]byte{m0, m1}, [][]byte{ai0, ai1})
59+
60+
eid, rh, err := newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, wrapped)
61+
require.NoError(t, err)
62+
assert.Equal(t, "wallet-42", eid)
63+
assert.Empty(t, rh)
64+
}
65+
66+
func TestPolicyEnrollmentIDSingleMember(t *testing.T) {
67+
m0, ai0 := newX509Member(t, "cert-zero", "wallet-7")
68+
policyID, wrapped := newPolicyIdentity(t, "$0", [][]byte{m0}, [][]byte{ai0})
69+
70+
eid, _, err := newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, wrapped)
71+
require.NoError(t, err)
72+
assert.Equal(t, "wallet-7", eid)
73+
}
74+
75+
func TestPolicyEnrollmentIDConflictingMembers(t *testing.T) {
76+
m0, ai0 := newX509Member(t, "cert-zero", "wallet-42")
77+
m1, ai1 := newX509Member(t, "cert-one", "wallet-43")
78+
policyID, wrapped := newPolicyIdentity(t, "$0 OR $1", [][]byte{m0, m1}, [][]byte{ai0, ai1})
79+
80+
eid, _, err := newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, wrapped)
81+
require.NoError(t, err)
82+
assert.Empty(t, eid)
83+
}
84+
85+
func TestPolicyEnrollmentIDEmptyMemberEID(t *testing.T) {
86+
// a member with no enrollment ID of its own: no common EID, not an error
87+
// (a nested composite spanning enrollments reports the same way)
88+
m0, ai0 := newX509Member(t, "cert-zero", "")
89+
policyID, wrapped := newPolicyIdentity(t, "$0", [][]byte{m0}, [][]byte{ai0})
90+
91+
eid, _, err := newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, wrapped)
92+
require.NoError(t, err)
93+
assert.Empty(t, eid)
94+
}
95+
96+
func TestPolicyEnrollmentIDNestedPolicy(t *testing.T) {
97+
// a member may itself be a policy identity, resolved recursively through
98+
// the parent multiplex deserializer
99+
cases := []struct {
100+
name string
101+
innerEID [2]string
102+
want string
103+
}{
104+
{"inner members share an enrollment", [2]string{"wallet-42", "wallet-42"}, "wallet-42"},
105+
{"inner members span enrollments", [2]string{"wallet-42", "wallet-43"}, ""},
106+
}
107+
for _, tc := range cases {
108+
t.Run(tc.name, func(t *testing.T) {
109+
m0, ai0 := newX509Member(t, "cert-zero", tc.innerEID[0])
110+
m1, ai1 := newX509Member(t, "cert-one", tc.innerEID[1])
111+
innerID, innerInfo := newPolicyIdentity(t, "$0 OR $1", [][]byte{m0, m1}, [][]byte{ai0, ai1})
112+
outerID, wrapped := newPolicyIdentity(t, "$0", [][]byte{innerID}, [][]byte{innerInfo})
113+
114+
eid, _, err := newEIDRHDeserializer().GetEIDAndRH(t.Context(), outerID, wrapped)
115+
require.NoError(t, err)
116+
assert.Equal(t, tc.want, eid)
117+
})
118+
}
119+
}
120+
121+
func TestPolicyEnrollmentIDUnresolvableMember(t *testing.T) {
122+
// malformed: member typed with an identity type that has no registered
123+
// deserializer
124+
m0, err := identity.WrapWithType(identity.Type(99), []byte("cert-zero"))
125+
require.NoError(t, err)
126+
ai0, err := (&x509.AuditInfo{EID: "wallet-42"}).Bytes()
127+
require.NoError(t, err)
128+
policyID, wrapped := newPolicyIdentity(t, "$0", [][]byte{m0}, [][]byte{ai0})
129+
130+
_, _, err = newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, wrapped)
131+
require.Error(t, err)
132+
assert.Contains(t, err.Error(), "failed to deserialize audit info of component")
133+
}
134+
135+
func TestPolicyEnrollmentIDMemberCountMismatch(t *testing.T) {
136+
// malformed: two members but a single component audit info
137+
m0, ai0 := newX509Member(t, "cert-zero", "wallet-42")
138+
m1, _ := newX509Member(t, "cert-one", "wallet-42")
139+
policyID, wrapped := newPolicyIdentity(t, "$0 OR $1", [][]byte{m0, m1}, [][]byte{ai0})
140+
141+
_, _, err := newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, wrapped)
142+
require.Error(t, err)
143+
assert.Contains(t, err.Error(), "component audit infos")
144+
}
145+
146+
func TestPolicyEnrollmentIDEmptyAuditInfoList(t *testing.T) {
147+
// malformed: one member but an empty audit info blob must still hit the
148+
// count check, not short-circuit to "no common EID"
149+
m0, _ := newX509Member(t, "cert-zero", "wallet-42")
150+
policyID, _ := newPolicyIdentity(t, "$0", [][]byte{m0}, [][]byte{[]byte("unused")})
151+
152+
_, _, err := newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, []byte(`{}`))
153+
require.Error(t, err)
154+
assert.Contains(t, err.Error(), "expected 1 component audit infos but received 0")
155+
}
156+
157+
func TestPolicyEnrollmentIDNoComponents(t *testing.T) {
158+
// malformed: a policy identity with no components at all
159+
inner, err := (&PolicyIdentity{Policy: "$0"}).Serialize()
160+
require.NoError(t, err)
161+
policyID, err := identity.WrapWithType(Policy, inner)
162+
require.NoError(t, err)
163+
164+
_, _, err = newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, []byte(`{}`))
165+
require.Error(t, err)
166+
assert.Contains(t, err.Error(), "no components")
167+
}
168+
169+
func TestPolicyEnrollmentIDZeroValueDeserializer(t *testing.T) {
170+
// zero-value deserializer (no inner): legacy behavior, empty enrollment ID
171+
m0, ai0 := newX509Member(t, "cert-zero", "wallet-42")
172+
policyID, wrapped := newPolicyIdentity(t, "$0", [][]byte{m0}, [][]byte{ai0})
173+
174+
d := deserializer.NewEIDRHDeserializer()
175+
d.AddDeserializer(Policy, &AuditInfoDeserializer{})
176+
177+
eid, rh, err := d.GetEIDAndRH(t.Context(), policyID, wrapped)
178+
require.NoError(t, err)
179+
assert.Empty(t, eid)
180+
assert.Empty(t, rh)
181+
}
182+
183+
func TestPolicyAuditInfoGarbageStillErrors(t *testing.T) {
184+
m0, _ := newX509Member(t, "cert-zero", "wallet-42")
185+
inner, err := (&PolicyIdentity{Policy: "$0", Identities: [][]byte{m0}}).Serialize()
186+
require.NoError(t, err)
187+
policyID, err := identity.WrapWithType(Policy, inner)
188+
require.NoError(t, err)
189+
190+
_, _, err = newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, []byte("not-json"))
191+
require.Error(t, err)
192+
}
193+
194+
func TestPolicyEnrollmentIDCrossEIDThenMalformedMember(t *testing.T) {
195+
// members legitimately span enrollments, but a later member is malformed:
196+
// the corruption must surface as an error, not be masked by the
197+
// cross-enrollment "" result
198+
unresolvable, err := identity.WrapWithType(identity.Type(99), []byte("cert-two"))
199+
require.NoError(t, err)
200+
unresolvableInfo, err := (&x509.AuditInfo{EID: "wallet-44"}).Bytes()
201+
require.NoError(t, err)
202+
203+
m0, ai0 := newX509Member(t, "cert-zero", "wallet-42")
204+
m1, ai1 := newX509Member(t, "cert-one", "wallet-43")
205+
policyID, wrapped := newPolicyIdentity(t, "$0 OR $1 OR $2",
206+
[][]byte{m0, m1, unresolvable}, [][]byte{ai0, ai1, unresolvableInfo})
207+
208+
_, _, err = newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, wrapped)
209+
require.Error(t, err)
210+
assert.Contains(t, err.Error(), "failed to deserialize audit info of component")
211+
}

0 commit comments

Comments
 (0)