Skip to content

Commit 3701dea

Browse files
authored
feat: Add support for multiple-status entries in VC (#95)
Multiple credentialStatus entries may be added to the VC so that both revocation and suspension may be supported for the VC. Signed-off-by: Bob Stasyszyn <bob.stasyszyn@gendigital.com>
1 parent c11d6ee commit 3701dea

File tree

11 files changed

+350
-51
lines changed

11 files changed

+350
-51
lines changed

presexch/definition_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3569,7 +3569,7 @@ type credentialProto struct {
35693569
Issuer *verifiable.Issuer
35703570
Issued *utiltime.TimeWrapper
35713571
Expired *utiltime.TimeWrapper
3572-
Status *verifiable.TypedID
3572+
Status []*verifiable.TypedID
35733573
Schemas []verifiable.TypedID
35743574
Evidence verifiable.Evidence
35753575
TermsOfUse []verifiable.TypedID

status/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Validator interface {
1616
ValidateStatus(vcStatus *verifiable.TypedID) error
1717
GetStatusVCURI(vcStatus *verifiable.TypedID) (string, error)
1818
GetStatusListIndex(vcStatus *verifiable.TypedID) (int, error)
19+
GetStatusPurpose(vcStatus *verifiable.TypedID) (string, error)
1920
}
2021

2122
// ValidatorGetter provides the matching Validator for a given credential status type.

status/status.go

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,17 @@ import (
1818
)
1919

2020
const (
21-
// RevokedMessage is the Client.VerifyStatus error message when the given verifiable.Credential is revoked.
22-
RevokedMessage = "revoked"
21+
// StatusPurposeRevocation is the purpose of the status list entry for revocation.
22+
StatusPurposeRevocation = "revocation"
23+
// StatusPurposeSuspension is the purpose of the status list entry for suspension.
24+
StatusPurposeSuspension = "suspension"
25+
)
26+
27+
var (
28+
// ErrRevoked is the Client.VerifyStatus error when the given verifiable.Credential is revoked.
29+
ErrRevoked = errors.New("revoked")
30+
// ErrSuspended is the Client.VerifyStatus error when the given verifiable.Credential is suspended.
31+
ErrSuspended = errors.New("suspended")
2332
)
2433

2534
// Client verifies revocation status for Verifiable Credentials.
@@ -28,31 +37,45 @@ type Client struct {
2837
Resolver api.StatusListVCURIResolver
2938
}
3039

31-
// VerifyStatus verifies the revocation status on the given Verifiable Credential, returning the errorstring "revoked"
32-
// if the given credential's status is revoked, nil if the credential is not revoked, and a different error if
33-
// verification fails.
40+
// VerifyStatus verifies the revocation status on the given Verifiable Credential, returning the errorstring:
41+
// - "revoked" if the given credential's status is revoked
42+
// - "suspended" if the given credential's status is suspended
43+
// - nil if the credential is not revoked or suspended, and a different error if verification fails.
3444
func (c *Client) VerifyStatus(credential *verifiable.Credential) error { //nolint:gocyclo
3545
contents := credential.Contents()
36-
if contents.Status == nil {
46+
if len(contents.Status) == 0 {
3747
return errors.New("vc missing status list field")
3848
}
3949

40-
validator, err := c.ValidatorGetter(contents.Status.Type)
50+
for _, status := range contents.Status {
51+
if err := c.verifyStatus(credential, status); err != nil {
52+
return err
53+
}
54+
}
55+
56+
return nil
57+
}
58+
59+
func (c *Client) verifyStatus( //nolint:gocyclo,funlen
60+
credential *verifiable.Credential,
61+
status *verifiable.TypedID,
62+
) error {
63+
validator, err := c.ValidatorGetter(status.Type)
4164
if err != nil {
4265
return err
4366
}
4467

45-
err = validator.ValidateStatus(contents.Status)
68+
err = validator.ValidateStatus(status)
4669
if err != nil {
4770
return err
4871
}
4972

50-
statusListIndex, err := validator.GetStatusListIndex(contents.Status)
73+
statusListIndex, err := validator.GetStatusListIndex(status)
5174
if err != nil {
5275
return err
5376
}
5477

55-
statusVCURL, err := validator.GetStatusVCURI(contents.Status)
78+
statusVCURL, err := validator.GetStatusVCURI(status)
5679
if err != nil {
5780
return err
5881
}
@@ -63,7 +86,8 @@ func (c *Client) VerifyStatus(credential *verifiable.Credential) error { //nolin
6386
}
6487

6588
statusListVCC := statusListVC.Contents()
66-
if statusListVCC.Issuer == nil || contents.Issuer == nil || statusListVCC.Issuer.ID != contents.Issuer.ID {
89+
if statusListVCC.Issuer == nil || credential.Contents().Issuer == nil ||
90+
statusListVCC.Issuer.ID != credential.Contents().Issuer.ID {
6791
return errors.New("issuer of the credential does not match status list vc issuer")
6892
}
6993

@@ -85,7 +109,19 @@ func (c *Client) VerifyStatus(credential *verifiable.Credential) error { //nolin
85109
}
86110

87111
if bitSet {
88-
return errors.New(RevokedMessage)
112+
purpose, err := validator.GetStatusPurpose(status)
113+
if err != nil {
114+
return err
115+
}
116+
117+
switch purpose {
118+
case StatusPurposeRevocation:
119+
return ErrRevoked
120+
case StatusPurposeSuspension:
121+
return ErrSuspended
122+
default:
123+
return fmt.Errorf("unsupported status purpose: %s", purpose)
124+
}
89125
}
90126

91127
return nil

status/status_test.go

Lines changed: 117 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030
const issuerID = "issuer-id"
3131

3232
func TestClient_VerifyStatus(t *testing.T) {
33-
t.Run("success", func(t *testing.T) {
33+
t.Run("single status -> success", func(t *testing.T) {
3434
client := Client{
3535
ValidatorGetter: validator.GetValidator,
3636
Resolver: resolver.NewResolver(http.DefaultClient, &vdr.VDRegistry{}, ""),
@@ -47,7 +47,7 @@ func TestClient_VerifyStatus(t *testing.T) {
4747
Issuer: &verifiable.Issuer{
4848
ID: issuerID,
4949
},
50-
Status: &verifiable.TypedID{
50+
Status: []*verifiable.TypedID{{
5151
ID: "foo-bar",
5252
Type: statuslist2021.StatusList2021Type,
5353
CustomFields: map[string]interface{}{
@@ -56,7 +56,7 @@ func TestClient_VerifyStatus(t *testing.T) {
5656
statuslist2021.StatusListIndex: "0",
5757
},
5858
},
59-
}))
59+
}}))
6060
require.NoError(t, err)
6161

6262
// status: revoked
@@ -65,18 +65,114 @@ func TestClient_VerifyStatus(t *testing.T) {
6565
ID: issuerID,
6666
},
6767

68-
Status: &verifiable.TypedID{
68+
Status: []*verifiable.TypedID{{
6969
ID: "foo-bar",
7070
Type: statuslist2021.StatusList2021Type,
7171
CustomFields: map[string]interface{}{
72-
statuslist2021.StatusPurpose: "foo",
72+
statuslist2021.StatusPurpose: StatusPurposeRevocation,
7373
statuslist2021.StatusListCredential: statusServer.URL,
7474
statuslist2021.StatusListIndex: "1",
7575
},
7676
},
77-
}))
78-
require.Error(t, err)
79-
require.Contains(t, err.Error(), RevokedMessage)
77+
}}))
78+
require.ErrorIs(t, err, ErrRevoked)
79+
})
80+
81+
t.Run("multi status -> success", func(t *testing.T) {
82+
client := Client{
83+
ValidatorGetter: validator.GetValidator,
84+
Resolver: resolver.NewResolver(http.DefaultClient, &vdr.VDRegistry{}, ""),
85+
}
86+
87+
statusServer := httptest.NewServer(mockStatusResponseHandler(t, mockStatusVC(t, issuerID, isRevoked{false, true})))
88+
89+
defer func() {
90+
statusServer.Close()
91+
}()
92+
93+
err := client.VerifyStatus(createTestCredential(t, verifiable.CredentialContents{
94+
Issuer: &verifiable.Issuer{
95+
ID: issuerID,
96+
},
97+
Status: []*verifiable.TypedID{
98+
{
99+
ID: "id1",
100+
Type: statuslist2021.StatusList2021Type,
101+
CustomFields: map[string]interface{}{
102+
statuslist2021.StatusPurpose: StatusPurposeRevocation,
103+
statuslist2021.StatusListCredential: statusServer.URL + "/revoked",
104+
statuslist2021.StatusListIndex: "0",
105+
},
106+
},
107+
{
108+
ID: "id2",
109+
Type: statuslist2021.StatusList2021Type,
110+
CustomFields: map[string]interface{}{
111+
statuslist2021.StatusPurpose: StatusPurposeSuspension,
112+
statuslist2021.StatusListCredential: statusServer.URL + "/suspended",
113+
statuslist2021.StatusListIndex: "0",
114+
},
115+
},
116+
}}))
117+
require.NoError(t, err)
118+
119+
t.Run("revoked", func(t *testing.T) {
120+
err = client.VerifyStatus(createTestCredential(t, verifiable.CredentialContents{
121+
Issuer: &verifiable.Issuer{
122+
ID: issuerID,
123+
},
124+
125+
Status: []*verifiable.TypedID{
126+
{
127+
ID: "id1",
128+
Type: statuslist2021.StatusList2021Type,
129+
CustomFields: map[string]interface{}{
130+
statuslist2021.StatusPurpose: StatusPurposeRevocation,
131+
statuslist2021.StatusListCredential: statusServer.URL + "/revoked",
132+
statuslist2021.StatusListIndex: "1",
133+
},
134+
},
135+
{
136+
ID: "id2",
137+
Type: statuslist2021.StatusList2021Type,
138+
CustomFields: map[string]interface{}{
139+
statuslist2021.StatusPurpose: StatusPurposeSuspension,
140+
statuslist2021.StatusListCredential: statusServer.URL + "/suspended",
141+
statuslist2021.StatusListIndex: "0",
142+
},
143+
},
144+
}}))
145+
require.ErrorIs(t, err, ErrRevoked)
146+
})
147+
148+
t.Run("suspended", func(t *testing.T) {
149+
err = client.VerifyStatus(createTestCredential(t, verifiable.CredentialContents{
150+
Issuer: &verifiable.Issuer{
151+
ID: issuerID,
152+
},
153+
154+
Status: []*verifiable.TypedID{
155+
{
156+
ID: "id1",
157+
Type: statuslist2021.StatusList2021Type,
158+
CustomFields: map[string]interface{}{
159+
statuslist2021.StatusPurpose: StatusPurposeRevocation,
160+
statuslist2021.StatusListCredential: statusServer.URL + "/revoked",
161+
statuslist2021.StatusListIndex: "0",
162+
},
163+
},
164+
{
165+
ID: "id2",
166+
Type: statuslist2021.StatusList2021Type,
167+
CustomFields: map[string]interface{}{
168+
statuslist2021.StatusPurpose: StatusPurposeSuspension,
169+
statuslist2021.StatusListCredential: statusServer.URL + "/suspended",
170+
statuslist2021.StatusListIndex: "1",
171+
},
172+
},
173+
}}))
174+
require.ErrorIs(t, err, ErrSuspended)
175+
})
80176
})
81177

82178
t.Run("fail", func(t *testing.T) {
@@ -96,7 +192,7 @@ func TestClient_VerifyStatus(t *testing.T) {
96192
},
97193
}
98194
err := client.VerifyStatus(createTestCredential(t, verifiable.CredentialContents{
99-
Status: &verifiable.TypedID{},
195+
Status: []*verifiable.TypedID{{}},
100196
}))
101197
require.Error(t, err)
102198
require.ErrorIs(t, err, expectErr)
@@ -113,7 +209,7 @@ func TestClient_VerifyStatus(t *testing.T) {
113209
},
114210
}
115211
err := client.VerifyStatus(createTestCredential(t, verifiable.CredentialContents{
116-
Status: &verifiable.TypedID{},
212+
Status: []*verifiable.TypedID{{}},
117213
}))
118214
require.Error(t, err)
119215
require.ErrorIs(t, err, expectErr)
@@ -130,7 +226,7 @@ func TestClient_VerifyStatus(t *testing.T) {
130226
},
131227
}
132228
err := client.VerifyStatus(createTestCredential(t, verifiable.CredentialContents{
133-
Status: &verifiable.TypedID{},
229+
Status: []*verifiable.TypedID{{}},
134230
}))
135231
require.Error(t, err)
136232
require.ErrorIs(t, err, expectErr)
@@ -147,7 +243,7 @@ func TestClient_VerifyStatus(t *testing.T) {
147243
},
148244
}
149245
err := client.VerifyStatus(createTestCredential(t, verifiable.CredentialContents{
150-
Status: &verifiable.TypedID{},
246+
Status: []*verifiable.TypedID{{}},
151247
}))
152248
require.Error(t, err)
153249
require.ErrorIs(t, err, expectErr)
@@ -165,7 +261,7 @@ func TestClient_VerifyStatus(t *testing.T) {
165261
},
166262
}
167263
err := client.VerifyStatus(createTestCredential(t, verifiable.CredentialContents{
168-
Status: &verifiable.TypedID{},
264+
Status: []*verifiable.TypedID{{}},
169265
}))
170266
require.Error(t, err)
171267
require.ErrorIs(t, err, expectErr)
@@ -188,7 +284,7 @@ func TestClient_VerifyStatus(t *testing.T) {
188284
Issuer: &verifiable.Issuer{
189285
ID: "foo",
190286
},
191-
Status: &verifiable.TypedID{},
287+
Status: []*verifiable.TypedID{{}},
192288
}))
193289
require.Error(t, err)
194290
require.Contains(t, err.Error(), "issuer of the credential does not match status list vc issuer")
@@ -216,7 +312,7 @@ func TestClient_VerifyStatus(t *testing.T) {
216312
},
217313
}
218314
err := client.VerifyStatus(createTestCredential(t, verifiable.CredentialContents{
219-
Status: &verifiable.TypedID{},
315+
Status: []*verifiable.TypedID{{}},
220316
Issuer: &verifiable.Issuer{
221317
ID: issuerID,
222318
},
@@ -233,6 +329,8 @@ type mockValidator struct {
233329
GetStatusVCURIErr error
234330
GetStatusListIndexVal int
235331
GetStatusListIndexErr error
332+
GetStatusPurposeVal string
333+
GetStatusPurposeErr error
236334
}
237335

238336
func (m *mockValidator) ValidateStatus(*verifiable.TypedID) error {
@@ -247,6 +345,10 @@ func (m *mockValidator) GetStatusListIndex(*verifiable.TypedID) (int, error) {
247345
return m.GetStatusListIndexVal, m.GetStatusListIndexErr
248346
}
249347

348+
func (m *mockValidator) GetStatusPurpose(vcStatus *verifiable.TypedID) (string, error) {
349+
return m.GetStatusPurposeVal, m.GetStatusPurposeErr
350+
}
351+
250352
type mockResolver struct {
251353
Cred *verifiable.Credential
252354
Err error

status/validator/statuslist2021/statuslist2021.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,13 @@ func (v *Validator) GetStatusListIndex(vcStatus *verifiable.TypedID) (int, error
9292

9393
return idx, nil
9494
}
95+
96+
// GetStatusPurpose returns the purpose of the status list. For example, "revocation", "suspension".
97+
func (v *Validator) GetStatusPurpose(vcStatus *verifiable.TypedID) (string, error) {
98+
statusPurpose, ok := vcStatus.CustomFields[StatusPurpose].(string)
99+
if !ok {
100+
return "", fmt.Errorf("%s must be a string", StatusPurpose)
101+
}
102+
103+
return statusPurpose, nil
104+
}

0 commit comments

Comments
 (0)