Skip to content

Commit 015b4c2

Browse files
authored
feat: support for bitstring status list (#97)
* feat: support for bitstring status list Signed-off-by: Volodymyr Kit <volodymyr.kit.ua@gmail.com> * feat: check statusSize Signed-off-by: Volodymyr Kit <volodymyr.kit.ua@gmail.com> * feat: use proper encoding for BitstringStatusListEntry Signed-off-by: Volodymyr Kit <volodymyr.kit.ua@gmail.com> * feat: remove redundant encoding check Signed-off-by: Volodymyr Kit <volodymyr.kit.ua@gmail.com> * feat: fix linter issues Signed-off-by: Volodymyr Kit <volodymyr.kit.ua@gmail.com> * feat: improve naming Signed-off-by: Volodymyr Kit <volodymyr.kit.ua@gmail.com> --------- Signed-off-by: Volodymyr Kit <volodymyr.kit.ua@gmail.com>
1 parent 489141c commit 015b4c2

File tree

8 files changed

+206
-7
lines changed

8 files changed

+206
-7
lines changed

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,6 @@ github.com/tidwall/sjson v1.1.4 h1:bTSsPLdAYF5QNLSwYsKfBKKTnlGbIuhqL3CpRsjzGhg=
129129
github.com/tidwall/sjson v1.1.4/go.mod h1:wXpKXu8CtDjKAZ+3DrKY5ROCorDFahq8l0tey/Lx1fg=
130130
github.com/trustbloc/bbs-signature-go v1.0.2 h1:gepEsbLiZHv/vva9FKG5gF38mGtOIyGez7desZxiI1o=
131131
github.com/trustbloc/bbs-signature-go v1.0.2/go.mod h1:xYotcXHAbcE0TO+SteW0J6XI3geQaXq4wdnXR2k+XCU=
132-
github.com/trustbloc/did-go v1.3.3-0.20250110131606-76c309e63d32 h1:aikcb3F09R4RFrayA/syZ/aLOutZn/DsdikQd2UWRh4=
133-
github.com/trustbloc/did-go v1.3.3-0.20250110131606-76c309e63d32/go.mod h1:bb8zEheJbCXvHGEzMGhbCcpiHEwcCZJ3Qua1G05WC4M=
134132
github.com/trustbloc/did-go v1.3.3-0.20250110150409-989a7364b77c h1:Gzb0MRUeAiTH+SBfjF0zhWIP5RRo8ozY1hTrXggY4lA=
135133
github.com/trustbloc/did-go v1.3.3-0.20250110150409-989a7364b77c/go.mod h1:bb8zEheJbCXvHGEzMGhbCcpiHEwcCZJ3Qua1G05WC4M=
136134
github.com/trustbloc/json-gold v0.5.2-0.20241206130328-d2135d9f36a8 h1:DomzdQu7D3CDBsMijT0E9uQl91iFcsIfYq1UKXmI/XQ=

status/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Validator interface {
1717
GetStatusVCURI(vcStatus *verifiable.TypedID) (string, error)
1818
GetStatusListIndex(vcStatus *verifiable.TypedID) (int, error)
1919
GetStatusPurpose(vcStatus *verifiable.TypedID) (string, error)
20+
MultiBaseEncoding() bool
2021
}
2122

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

status/internal/bitstring/bitstring.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import (
1313
"compress/gzip"
1414
"encoding/base64"
1515
"errors"
16+
"fmt"
17+
18+
"github.com/multiformats/go-multibase"
1619
)
1720

1821
const (
@@ -21,10 +24,27 @@ const (
2124
)
2225

2326
// Decode decodes a compressed bitstring from a base64URL-encoded string.
24-
func Decode(src string) ([]byte, error) {
25-
decodedBits, err := base64.RawURLEncoding.DecodeString(src)
26-
if err != nil {
27-
return nil, err
27+
func Decode(src string, opts ...Opt) ([]byte, error) {
28+
options := &options{}
29+
30+
for _, opt := range opts {
31+
opt(options)
32+
}
33+
34+
var decodedBits []byte
35+
36+
var err error
37+
38+
if options.multiBaseEncoding {
39+
_, decodedBits, err = multibase.Decode(src)
40+
if err != nil {
41+
return nil, fmt.Errorf("decode: %w", err)
42+
}
43+
} else {
44+
decodedBits, err = base64.RawURLEncoding.DecodeString(src)
45+
if err != nil {
46+
return nil, err
47+
}
2848
}
2949

3050
b := bytes.NewReader(decodedBits)
@@ -71,3 +91,16 @@ func Encode(bitString []byte) (string, error) {
7191

7292
return base64.RawURLEncoding.EncodeToString(buf.Bytes()), nil
7393
}
94+
95+
type Opt func(*options)
96+
97+
type options struct {
98+
multiBaseEncoding bool
99+
}
100+
101+
// WithMultiBaseEncoding sets support of multiBase encoding.
102+
func WithMultiBaseEncoding(multiBaseEncoding bool) Opt {
103+
return func(options *options) {
104+
options.multiBaseEncoding = multiBaseEncoding
105+
}
106+
}

status/status.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func (c *Client) verifyStatus( //nolint:gocyclo,funlen
9898
return errors.New("encodedList must be a string")
9999
}
100100

101-
bitString, err := bitstring.Decode(encodedList)
101+
bitString, err := bitstring.Decode(encodedList, bitstring.WithMultiBaseEncoding(validator.MultiBaseEncoding()))
102102
if err != nil {
103103
return fmt.Errorf("failed to decode bits: %w", err)
104104
}

status/status_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ func (m *mockValidator) GetStatusPurpose(vcStatus *verifiable.TypedID) (string,
349349
return m.GetStatusPurposeVal, m.GetStatusPurposeErr
350350
}
351351

352+
func (v *mockValidator) MultiBaseEncoding() bool {
353+
return false
354+
}
355+
352356
type mockResolver struct {
353357
Cred *verifiable.Credential
354358
Err error
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
Copyright Avast Software. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
// Package bitstringstatus handles client-side validation and parsing for
8+
// Credential Status fields of type BitstringStatusList, as per spec: https://www.w3.org/TR/vc-bitstring-status-list/
9+
package bitstringstatus
10+
11+
import (
12+
"errors"
13+
"fmt"
14+
"strconv"
15+
16+
"github.com/trustbloc/vc-go/verifiable"
17+
)
18+
19+
const (
20+
// BitstringStatusListType represents the implementation of Bitstring Status List.
21+
// VC.Status.Type
22+
// Doc: https://www.w3.org/TR/vc-bitstring-status-list/#bitstringstatuslistentry
23+
BitstringStatusListType = "BitstringStatusListEntry"
24+
25+
// StatusListCredential stores the link to the status list VC.
26+
// VC.Status.CustomFields key.
27+
StatusListCredential = "statusListCredential"
28+
29+
// StatusListIndex identifies the bit position of the status value of the VC.
30+
// VC.Status.CustomFields key.
31+
StatusListIndex = "statusListIndex"
32+
33+
// StatusPurpose for BitstringStatusList.
34+
// VC.Status.CustomFields key. Only "revocation" value is supported.
35+
// TODO: check if it's really only 'revocation'. Spec allows: refresh, revocation, suspension, message.
36+
StatusPurpose = "statusPurpose"
37+
// StatusSize indicates the size of the status entry in bits.
38+
StatusSize = "statusSize"
39+
// StatusMessage represents custom descriptive messages about the status of the verifiable credential.
40+
StatusMessage = "statusMessage"
41+
)
42+
43+
// Validator validates a Verifiable Credential's Status field against the BitstringStatusList specification, and
44+
// returns fields for status verification.
45+
//
46+
// Implements spec: https://www.w3.org/TR/vc-bitstring-status-list/#bitstringstatuslistentry
47+
type Validator struct{}
48+
49+
// ValidateStatus validates that a Verifiable Credential's Status field matches the BitstringStatusList specification.
50+
func (v *Validator) ValidateStatus(vcStatus *verifiable.TypedID) error {
51+
if vcStatus == nil {
52+
return errors.New("vc status does not exist")
53+
}
54+
55+
if vcStatus.Type != BitstringStatusListType {
56+
return fmt.Errorf("vc status %s not supported", vcStatus.Type)
57+
}
58+
59+
for _, field := range []string{StatusListCredential, StatusListIndex, StatusPurpose} {
60+
if err := isMissingField(vcStatus, field); err != nil {
61+
return err
62+
}
63+
}
64+
65+
err := checkStatusSize(vcStatus)
66+
if err != nil {
67+
return err
68+
}
69+
70+
return nil
71+
}
72+
73+
func isMissingField(vcStatus *verifiable.TypedID, field string) error {
74+
if vcStatus.CustomFields[field] == nil {
75+
return fmt.Errorf("%s field does not exist in vc status", field)
76+
}
77+
78+
return nil
79+
}
80+
81+
func checkStatusSize(vcStatus *verifiable.TypedID) error {
82+
statusSizeRaw := vcStatus.CustomFields[StatusSize]
83+
if statusSizeRaw == nil {
84+
return nil
85+
}
86+
87+
statusSizeF, ok := statusSizeRaw.(float64)
88+
if !ok {
89+
return errors.New("statusSize must be an integer")
90+
}
91+
92+
statusSize := int(statusSizeF)
93+
94+
if statusSize <= 0 {
95+
return fmt.Errorf("statusSize must be greater than 0, but got %d", statusSize)
96+
}
97+
98+
if statusSize == 1 {
99+
return nil
100+
}
101+
102+
possibleStatusSizes := 1<<statusSize - 1
103+
104+
statusMessages, ok := vcStatus.CustomFields[StatusMessage].([]any)
105+
if !ok {
106+
return fmt.Errorf("%s must be an array", StatusMessage)
107+
}
108+
109+
if len(statusMessages) != possibleStatusSizes {
110+
return fmt.Errorf("the length of %s must be equal to %d", StatusMessage, possibleStatusSizes)
111+
}
112+
113+
return nil
114+
}
115+
116+
// GetStatusVCURI returns the ID (URL) of status VC.
117+
func (v *Validator) GetStatusVCURI(vcStatus *verifiable.TypedID) (string, error) {
118+
statusListVC, ok := vcStatus.CustomFields[StatusListCredential].(string)
119+
if !ok {
120+
return "", errors.New("failed to cast URI of statusListCredential")
121+
}
122+
123+
return statusListVC, nil
124+
}
125+
126+
// GetStatusListIndex returns the bit position of the status value of the VC.
127+
func (v *Validator) GetStatusListIndex(vcStatus *verifiable.TypedID) (int, error) {
128+
statusListIndex, ok := vcStatus.CustomFields[StatusListIndex].(string)
129+
if !ok {
130+
return -1, fmt.Errorf("%s must be a string", StatusListIndex)
131+
}
132+
133+
idx, err := strconv.Atoi(statusListIndex)
134+
if err != nil {
135+
return -1, fmt.Errorf("unable to get statusListIndex: %w", err)
136+
}
137+
138+
return idx, nil
139+
}
140+
141+
// GetStatusPurpose returns the purpose of the status list. For example, "revocation", "suspension".
142+
func (v *Validator) GetStatusPurpose(vcStatus *verifiable.TypedID) (string, error) {
143+
statusPurpose, ok := vcStatus.CustomFields[StatusPurpose].(string)
144+
if !ok {
145+
return "", fmt.Errorf("%s must be a string", StatusPurpose)
146+
}
147+
148+
return statusPurpose, nil
149+
}
150+
151+
// MultiBaseEncoding indicates that status uses MultiBase encoding.
152+
// See https://www.w3.org/TR/cid-1.0/#multibase-0 for more details.
153+
func (v *Validator) MultiBaseEncoding() bool {
154+
return true
155+
}

status/validator/statuslist2021/statuslist2021.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,8 @@ func (v *Validator) GetStatusPurpose(vcStatus *verifiable.TypedID) (string, erro
102102

103103
return statusPurpose, nil
104104
}
105+
106+
// MultiBaseEncoding indicates that status uses MultiBase encoding.
107+
func (v *Validator) MultiBaseEncoding() bool {
108+
return false
109+
}

status/validator/validator.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"fmt"
1313

1414
"github.com/trustbloc/vc-go/status/api"
15+
"github.com/trustbloc/vc-go/status/validator/bitstringstatus"
1516
"github.com/trustbloc/vc-go/status/validator/statuslist2021"
1617
)
1718

@@ -20,6 +21,8 @@ func GetValidator(statusType string) (api.Validator, error) { //nolint:ireturn
2021
switch statusType {
2122
case statuslist2021.StatusList2021Type:
2223
return &statuslist2021.Validator{}, nil
24+
case bitstringstatus.BitstringStatusListType:
25+
return &bitstringstatus.Validator{}, nil
2326
default:
2427
return nil, fmt.Errorf("unsupported VCStatusListType %s", statusType)
2528
}

0 commit comments

Comments
 (0)