Skip to content

Commit a52cc93

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 a52cc93

8 files changed

Lines changed: 337 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: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
// fuzzMember returns a typed x509 member identity and its audit info bytes.
18+
func fuzzMember(f *testing.F, name, eid string) ([]byte, []byte) {
19+
f.Helper()
20+
member, err := identity.WrapWithType(x509.IdentityType, []byte(name))
21+
require.NoError(f, err)
22+
auditInfo, err := (&x509.AuditInfo{EID: eid, RH: []byte("rh-" + eid)}).Bytes()
23+
require.NoError(f, err)
24+
25+
return member, auditInfo
26+
}
27+
28+
// fuzzPolicy wraps members into a policy identity (inner DER and typed
29+
// envelope) and the matching composite audit info blob.
30+
func fuzzPolicy(f *testing.F, policy string, members, auditInfos [][]byte) (inner, envelope, wrapped []byte) {
31+
f.Helper()
32+
inner, err := (&PolicyIdentity{Policy: policy, Identities: members}).Serialize()
33+
require.NoError(f, err)
34+
envelope, err = identity.WrapWithType(Policy, inner)
35+
require.NoError(f, err)
36+
wrapped, err = WrapAuditInfo(auditInfos)
37+
require.NoError(f, err)
38+
39+
return inner, envelope, wrapped
40+
}
41+
42+
func FuzzDeserializeAuditInfoNoPanic(f *testing.F) {
43+
m0, ai0 := fuzzMember(f, "cert-zero", "wallet-42")
44+
m1, ai1 := fuzzMember(f, "cert-one", "wallet-43")
45+
46+
// valid single- and two-member policies, both identity encodings
47+
inner1, envelope1, wrapped1 := fuzzPolicy(f, "$0", [][]byte{m0}, [][]byte{ai0})
48+
f.Add(inner1, wrapped1)
49+
f.Add(envelope1, wrapped1)
50+
inner2, _, wrapped2 := fuzzPolicy(f, "$0 OR $1", [][]byte{m0, m1}, [][]byte{ai0, ai1})
51+
f.Add(inner2, wrapped2)
52+
// empty and truncated inputs
53+
f.Add([]byte{}, []byte{})
54+
f.Add(inner2[:len(inner2)/2], wrapped2[:len(wrapped2)/2])
55+
f.Add(inner2, []byte(`{"IdentityAuditInfos":`))
56+
// member count mismatch
57+
innerMismatch, _, wrappedOne := fuzzPolicy(f, "$0 OR $1", [][]byte{m0, m1}, [][]byte{ai0})
58+
f.Add(innerMismatch, wrappedOne)
59+
// unknown member identity type
60+
unknown, err := identity.WrapWithType(identity.Type(99), []byte("cert-x"))
61+
require.NoError(f, err)
62+
innerUnknown, _, wrappedUnknown := fuzzPolicy(f, "$0", [][]byte{unknown}, [][]byte{ai0})
63+
f.Add(innerUnknown, wrappedUnknown)
64+
// nested and deeply nested policies
65+
nestedID, nestedInfo := m0, ai0
66+
for range 5 {
67+
_, nestedID, nestedInfo = fuzzPolicy(f, "$0", [][]byte{nestedID}, [][]byte{nestedInfo})
68+
}
69+
innerDeep, _, wrappedDeep := fuzzPolicy(f, "$0", [][]byte{nestedID}, [][]byte{nestedInfo})
70+
f.Add(innerDeep, wrappedDeep)
71+
72+
f.Fuzz(func(t *testing.T, rawID, rawInfo []byte) {
73+
require.NotPanics(t, func() {
74+
_, _ = NewAuditInfoDeserializer(newEIDRHDeserializer()).DeserializeAuditInfo(t.Context(), rawID, rawInfo)
75+
})
76+
})
77+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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 TestPolicyEnrollmentIDZeroValueDeserializer(t *testing.T) {
147+
// zero-value deserializer (no inner): legacy behavior, empty enrollment ID
148+
m0, ai0 := newX509Member(t, "cert-zero", "wallet-42")
149+
policyID, wrapped := newPolicyIdentity(t, "$0", [][]byte{m0}, [][]byte{ai0})
150+
151+
d := deserializer.NewEIDRHDeserializer()
152+
d.AddDeserializer(Policy, &AuditInfoDeserializer{})
153+
154+
eid, rh, err := d.GetEIDAndRH(t.Context(), policyID, wrapped)
155+
require.NoError(t, err)
156+
assert.Empty(t, eid)
157+
assert.Empty(t, rh)
158+
}
159+
160+
func TestPolicyAuditInfoGarbageStillErrors(t *testing.T) {
161+
m0, _ := newX509Member(t, "cert-zero", "wallet-42")
162+
inner, err := (&PolicyIdentity{Policy: "$0", Identities: [][]byte{m0}}).Serialize()
163+
require.NoError(t, err)
164+
policyID, err := identity.WrapWithType(Policy, inner)
165+
require.NoError(t, err)
166+
167+
_, _, err = newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, []byte("not-json"))
168+
require.Error(t, err)
169+
}
170+
171+
func TestPolicyEnrollmentIDCrossEIDThenMalformedMember(t *testing.T) {
172+
// members legitimately span enrollments, but a later member is malformed:
173+
// the corruption must surface as an error, not be masked by the
174+
// cross-enrollment "" result
175+
unresolvable, err := identity.WrapWithType(identity.Type(99), []byte("cert-two"))
176+
require.NoError(t, err)
177+
unresolvableInfo, err := (&x509.AuditInfo{EID: "wallet-44"}).Bytes()
178+
require.NoError(t, err)
179+
180+
m0, ai0 := newX509Member(t, "cert-zero", "wallet-42")
181+
m1, ai1 := newX509Member(t, "cert-one", "wallet-43")
182+
policyID, wrapped := newPolicyIdentity(t, "$0 OR $1 OR $2",
183+
[][]byte{m0, m1, unresolvable}, [][]byte{ai0, ai1, unresolvableInfo})
184+
185+
_, _, err = newEIDRHDeserializer().GetEIDAndRH(t.Context(), policyID, wrapped)
186+
require.Error(t, err)
187+
assert.Contains(t, err.Error(), "failed to deserialize audit info of component")
188+
}

token/services/identity/boolpolicy/deserializer.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,65 @@ func (d *TypedIdentityDeserializer) Recipients(id driver.Identity, typ identity.
154154

155155
// AuditInfoDeserializer deserialises raw audit info bytes into the AuditInfo
156156
// struct for the enrollment-ID / revocation-handle path.
157-
type AuditInfoDeserializer struct{}
157+
// It derives the policy identity's enrollment ID from its components.
158+
type AuditInfoDeserializer struct {
159+
inner driver2.AuditInfoDeserializer
160+
}
161+
162+
// NewAuditInfoDeserializer constructs an AuditInfoDeserializer resolving
163+
// per-component audit infos through inner, typically the parent multiplex
164+
// deserializer.
165+
func NewAuditInfoDeserializer(inner driver2.AuditInfoDeserializer) *AuditInfoDeserializer {
166+
return &AuditInfoDeserializer{inner: inner}
167+
}
158168

159-
func (a *AuditInfoDeserializer) DeserializeAuditInfo(_ context.Context, _ driver.Identity, raw []byte) (driver2.AuditInfo, error) {
169+
// DeserializeAuditInfo decodes raw policy audit info and derives its enrollment ID.
170+
func (a *AuditInfoDeserializer) DeserializeAuditInfo(ctx context.Context, id driver.Identity, raw []byte) (driver2.AuditInfo, error) {
160171
ei := &AuditInfo{}
161172
if err := json.Unmarshal(raw, ei); err != nil {
162173
return nil, err
163174
}
175+
eid, err := a.commonEnrollmentID(ctx, id, ei)
176+
if err != nil {
177+
return nil, errors.WithMessagef(err, "failed deriving policy enrollment ID")
178+
}
179+
ei.eid = eid
164180

165181
return ei, nil
166182
}
183+
184+
// commonEnrollmentID returns the enrollment ID shared by all components:
185+
// "" when a component has none (e.g. a nested composite) or they disagree,
186+
// an error on unresolvable audit info or a component count mismatch.
187+
func (a *AuditInfoDeserializer) commonEnrollmentID(ctx context.Context, id driver.Identity, ei *AuditInfo) (string, error) {
188+
if a.inner == nil || len(ei.IdentityAuditInfos) == 0 {
189+
return "", nil
190+
}
191+
pi := PolicyIdentity{}
192+
if err := pi.Deserialize(id); err != nil {
193+
return "", errors.Wrapf(err, "failed to deserialize policy identity")
194+
}
195+
if len(pi.Identities) != len(ei.IdentityAuditInfos) {
196+
return "", errors.Errorf("expected %d component audit infos but received %d",
197+
len(pi.Identities), len(ei.IdentityAuditInfos))
198+
}
199+
// resolve every component before declaring a result so corruption in a
200+
// later component is not masked by an earlier "no common EID" outcome
201+
eids := make([]string, len(ei.IdentityAuditInfos))
202+
for k, info := range ei.IdentityAuditInfos {
203+
memberAuditInfo, err := a.inner.DeserializeAuditInfo(ctx, pi.Identities[k], info.AuditInfo)
204+
if err != nil {
205+
return "", errors.Wrapf(err, "failed to deserialize audit info of component [%d]", k)
206+
}
207+
eids[k] = memberAuditInfo.EnrollmentID()
208+
}
209+
for _, memberEID := range eids {
210+
if memberEID == "" || memberEID != eids[0] {
211+
// no common EID: a member has none (e.g. a nested composite
212+
// spanning enrollments) or members belong to different ones
213+
return "", nil
214+
}
215+
}
216+
217+
return eids[0], nil
218+
}

token/services/identity/boolpolicy/identity.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,21 @@ type IdentityAuditInfo struct {
9595
// It is a sequence of per-component audit infos in the same order as Identities.
9696
type AuditInfo struct {
9797
IdentityAuditInfos []IdentityAuditInfo
98+
// eid is the enrollment ID shared by all component identities;
99+
// empty when they span enrollments.
100+
eid string
98101
}
99102

100-
func (a *AuditInfo) EnrollmentID() string { return "" }
103+
// EnrollmentID returns the enrollment ID shared by all component identities,
104+
// or "" when there is none.
105+
func (a *AuditInfo) EnrollmentID() string { return a.eid }
106+
107+
// RevocationHandle returns "": a policy identity has no revocation handle of
108+
// its own.
101109
func (a *AuditInfo) RevocationHandle() string { return "" }
102-
func (a *AuditInfo) Bytes() ([]byte, error) { return json.Marshal(a) }
110+
111+
// Bytes returns the JSON encoding of the AuditInfo.
112+
func (a *AuditInfo) Bytes() ([]byte, error) { return json.Marshal(a) }
103113

104114
// WrapAuditInfo packs per-component audit info bytes into a single blob.
105115
func WrapAuditInfo(recipients [][]byte) ([]byte, error) {

0 commit comments

Comments
 (0)