Skip to content

Commit

Permalink
feat: BitstringStatusList for VC model 2.0 (#1763)
Browse files Browse the repository at this point in the history
Added support for BitstringStatusListEntry status list type which is compatible with VC model 2.0.

Signed-off-by: Bob Stasyszyn <bob.stasyszyn@gendigital.com>
  • Loading branch information
bstasyszyn authored Sep 25, 2024
1 parent b6ca73e commit 3919504
Show file tree
Hide file tree
Showing 22 changed files with 950 additions and 640 deletions.
150 changes: 142 additions & 8 deletions component/credentialstatus/credentialstatus_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const (
eventTopic = "testEventTopic"
)

func validateVCStatus(
func validateVCStatusList2021Entry(
t *testing.T, s *Service, statusID *credentialstatus.StatusListEntry, expectedListID credentialstatus.ListID) {
t.Helper()

Expand Down Expand Up @@ -94,6 +94,43 @@ func validateVCStatus(
require.False(t, bitSet)
}

func validateBitstringStatusListEntry(
t *testing.T, s *Service, statusID *credentialstatus.StatusListEntry, expectedListID credentialstatus.ListID) {
t.Helper()

require.Equal(t, string(vc.BitstringStatusList), statusID.TypedID.Type)
require.Equal(t, "revocation", statusID.TypedID.CustomFields[statustype.StatusPurpose].(string))

existingStatusListVCID, ok := statusID.TypedID.CustomFields[statustype.StatusListCredential].(string)
require.True(t, ok)

chunks := strings.Split(existingStatusListVCID, "/")
existingStatusVCListID := chunks[len(chunks)-1]
require.Equal(t, string(expectedListID), existingStatusVCListID)

statusListVC, err := s.GetStatusListVC(context.Background(), externalProfileID, existingStatusVCListID)
require.NoError(t, err)

statusListVCC := statusListVC.Contents()

require.Equal(t, existingStatusListVCID, statusListVCC.ID)
require.Equal(t, "did:test:abc", statusListVCC.Issuer.ID)
require.Equal(t, verifiable.V2ContextURI, statusListVCC.Context[0])
credSubject := statusListVCC.Subject
require.Equal(t, existingStatusListVCID+"#list", credSubject[0].ID)
require.Equal(t, statustype.StatusListBitstringVCSubjectType, credSubject[0].CustomFields["type"].(string))
require.Equal(t, "revocation", credSubject[0].CustomFields[statustype.StatusPurpose].(string))
require.NotEmpty(t, credSubject[0].CustomFields["encodedList"].(string))
bitString, err := bitstring.DecodeBits(credSubject[0].CustomFields["encodedList"].(string))
require.NoError(t, err)

revocationListIndex, err := strconv.Atoi(statusID.TypedID.CustomFields[statustype.StatusListIndex].(string))
require.NoError(t, err)
bitSet, err := bitString.Get(revocationListIndex)
require.NoError(t, err)
require.False(t, bitSet)
}

func TestCredentialStatusList_CreateStatusListEntry(t *testing.T) {
t.Run("test success", func(t *testing.T) {
loader := testutil.DocumentLoader(t)
Expand Down Expand Up @@ -140,11 +177,104 @@ func TestCredentialStatusList_CreateStatusListEntry(t *testing.T) {

statusID, err := s.CreateStatusListEntry(ctx, profileID, profileVersion, credID)
require.NoError(t, err)
validateVCStatus(t, s, statusID, listID)
validateVCStatusList2021Entry(t, s, statusID, listID)

statusID, err = s.CreateStatusListEntry(ctx, profileID, profileVersion, credID)
require.NoError(t, err)
validateVCStatusList2021Entry(t, s, statusID, listID)

// List size equals 2, so after 2 issuances CSL encodedBitString is full and listID must be updated.
updatedListID, err := cslIndexStore.GetLatestListID(ctx)
require.NoError(t, err)
require.NotEqual(t, updatedListID, listID)

statusID, err = s.CreateStatusListEntry(ctx, profileID, profileVersion, credID)
require.NoError(t, err)
validateVCStatusList2021Entry(t, s, statusID, updatedListID)

statusID, err = s.CreateStatusListEntry(ctx, profileID, profileVersion, credID)
require.NoError(t, err)
validateVCStatusList2021Entry(t, s, statusID, updatedListID)

// List size equals 2, so after 4 issuances CSL encodedBitString is full and listID must be updated.
updatedListIDSecond, err := cslIndexStore.GetLatestListID(ctx)
require.NoError(t, err)
require.NotEqual(t, updatedListID, updatedListIDSecond)
require.NotEqual(t, listID, updatedListIDSecond)

statusID, err = s.CreateStatusListEntry(ctx, profileID, profileVersion, credID)
require.NoError(t, err)
validateVCStatusList2021Entry(t, s, statusID, updatedListIDSecond)
})

t.Run("test error get profile service", func(t *testing.T) {
mockProfileSrv := NewMockProfileService(gomock.NewController(t))
mockProfileSrv.EXPECT().GetProfile(profileID, profileVersion).Times(1).Return(nil, errors.New("some error"))

s, err := New(&Config{
ProfileService: mockProfileSrv,
})
require.NoError(t, err)

status, err := s.CreateStatusListEntry(context.Background(), profileID, profileVersion, credID)
require.Error(t, err)
require.Nil(t, status)
require.Contains(t, err.Error(), "get profile")
})
}

func TestCredentialStatusList_CreateStatusListEntry_Bitstring(t *testing.T) {
t.Run("test success", func(t *testing.T) {
loader := testutil.DocumentLoader(t)
mockProfileSrv := NewMockProfileService(gomock.NewController(t))
mockProfileSrv.EXPECT().GetProfile(profileID, profileVersion).AnyTimes().
Return(getTestProfileEx(vc.BitstringStatusList, vcsverifiable.Ed25519Signature2018), nil)
mockKMSRegistry := NewMockKMSRegistry(gomock.NewController(t))
mockKMSRegistry.EXPECT().GetKeyManager(gomock.Any()).Times(5).Return(&vcskms.MockKMS{}, nil)
ctx := context.Background()

cslVCStore := newMockCSLVCStore()

cslIndexStore := newMockCSLIndexStore()

listID, err := cslIndexStore.GetLatestListID(ctx)
require.NoError(t, err)

vcStatusStore := newMockVCStatusStore()

cslMgr, err := cslmanager.New(
&cslmanager.Config{
CSLVCStore: cslVCStore,
CSLIndexStore: cslIndexStore,
VCStatusStore: vcStatusStore,
ListSize: 2,
KMSRegistry: mockKMSRegistry,
ExternalURL: "https://localhost:8080",
Crypto: vccrypto.New(
&vdrmock.VDRegistry{ResolveValue: createDIDDoc("did:test:abc")}, loader),
})
require.NoError(t, err)

s, err := New(&Config{
DocumentLoader: loader,
CSLManager: cslMgr,
CSLVCStore: cslVCStore,
VCStatusStore: vcStatusStore,
ProfileService: mockProfileSrv,
KMSRegistry: mockKMSRegistry,
ExternalURL: "https://localhost:8080",
Crypto: vccrypto.New(
&vdrmock.VDRegistry{ResolveValue: createDIDDoc("did:test:abc")}, loader),
})
require.NoError(t, err)

statusID, err := s.CreateStatusListEntry(ctx, profileID, profileVersion, credID)
require.NoError(t, err)
validateBitstringStatusListEntry(t, s, statusID, listID)

statusID, err = s.CreateStatusListEntry(ctx, profileID, profileVersion, credID)
require.NoError(t, err)
validateVCStatus(t, s, statusID, listID)
validateBitstringStatusListEntry(t, s, statusID, listID)

// List size equals 2, so after 2 issuances CSL encodedBitString is full and listID must be updated.
updatedListID, err := cslIndexStore.GetLatestListID(ctx)
Expand All @@ -153,11 +283,11 @@ func TestCredentialStatusList_CreateStatusListEntry(t *testing.T) {

statusID, err = s.CreateStatusListEntry(ctx, profileID, profileVersion, credID)
require.NoError(t, err)
validateVCStatus(t, s, statusID, updatedListID)
validateBitstringStatusListEntry(t, s, statusID, updatedListID)

statusID, err = s.CreateStatusListEntry(ctx, profileID, profileVersion, credID)
require.NoError(t, err)
validateVCStatus(t, s, statusID, updatedListID)
validateBitstringStatusListEntry(t, s, statusID, updatedListID)

// List size equals 2, so after 4 issuances CSL encodedBitString is full and listID must be updated.
updatedListIDSecond, err := cslIndexStore.GetLatestListID(ctx)
Expand All @@ -167,7 +297,7 @@ func TestCredentialStatusList_CreateStatusListEntry(t *testing.T) {

statusID, err = s.CreateStatusListEntry(ctx, profileID, profileVersion, credID)
require.NoError(t, err)
validateVCStatus(t, s, statusID, updatedListIDSecond)
validateBitstringStatusListEntry(t, s, statusID, updatedListIDSecond)
})

t.Run("test error get profile service", func(t *testing.T) {
Expand Down Expand Up @@ -875,17 +1005,21 @@ func validateEvent(e *spi.Event) error {
}

func getTestProfile() *profileapi.Issuer {
return getTestProfileEx(vc.StatusList2021VCStatus, vcsverifiable.Ed25519Signature2018)
}

func getTestProfileEx(statusListType vc.StatusType, sigType vcsverifiable.SignatureType) *profileapi.Issuer {
return &profileapi.Issuer{
ID: profileID,
Version: profileVersion,
Name: "testprofile",
GroupID: "externalID",
VCConfig: &profileapi.VCConfig{
Format: vcsverifiable.Ldp,
SigningAlgorithm: "Ed25519Signature2018",
SigningAlgorithm: sigType,
KeyType: kms.ED25519Type,
Status: profileapi.StatusConfig{
Type: vc.StatusList2021VCStatus,
Type: statusListType,
},
},
SigningDID: &profileapi.SigningDID{
Expand Down
2 changes: 1 addition & 1 deletion pkg/cslmanager/cslmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (s *Manager) CreateCSLEntry(
}

statusListEntry := &credentialstatus.StatusListEntry{
TypedID: vcStatusProcessor.CreateVCStatus(strconv.Itoa(statusBitIndex), cslURL),
TypedID: vcStatusProcessor.CreateVCStatus(strconv.Itoa(statusBitIndex), cslURL, statustype.StatusPurposeRevocation),
Context: vcStatusProcessor.GetVCContext(),
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/cslmanager/cslmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ func TestCredentialStatusList_CreateCSLEntry(t *testing.T) {
cslIndexStore := newMockCSLIndexStore()
cslVCStore := newMockCSLVCStore()

statusProcessor, err := statustype.GetVCStatusProcessor(vc.StatusList2021VCStatus)
statusProcessor, err := statustype.GetVCStatusProcessor(vc.BitstringStatusList)
require.NoError(t, err)

listID, err := cslIndexStore.GetLatestListID(context.Background())
Expand Down
13 changes: 12 additions & 1 deletion pkg/doc/vc/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,26 @@ const (
// VC > Status > Type
// Doc: https://w3c-ccg.github.io/vc-status-rl-2020/
RevocationList2020VCStatus StatusType = "RevocationList2020Status"

// BitstringStatusList represents the implementation of the Bitstring VC Status List.
// VC > Status > Type
// Doc: https://www.w3.org/TR/vc-bitstring-status-list/
BitstringStatusList StatusType = "BitstringStatusListEntry"
)

// Field is used to define the key-value pair for additional fields in VC.
type Field struct {
Key string
Value interface{}
}

// StatusProcessor holds the list of methods required for processing different versions of Status(Revocation) List VC.
type StatusProcessor interface {
ValidateStatus(vcStatus *verifiable.TypedID) error
GetStatusVCURI(vcStatus *verifiable.TypedID) (string, error)
GetStatusListIndex(vcStatus *verifiable.TypedID) (int, error)
CreateVC(vcID string, listSize int, profile *Signer) (*verifiable.Credential, error)
CreateVCStatus(statusListIndex string, vcID string) *verifiable.TypedID
CreateVCStatus(index, vcID, purpose string, additionalFields ...Field) *verifiable.TypedID
GetVCContext() string
}

Expand Down
24 changes: 23 additions & 1 deletion pkg/doc/vc/statustype/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ package statustype

import "github.com/trustbloc/vc-go/verifiable"

const (
vcType = "VerifiableCredential"

// StatusListIndex identifies the bit position of the status value of the VC.
// VC > Status > CustomFields key.
StatusListIndex = "statusListIndex"
// StatusListCredential stores the link to the status list VC.
// VC > Status > CustomFields key.
StatusListCredential = "statusListCredential"
// StatusPurpose for StatusList2021.
// VC > Status > CustomFields key. Only "revocation" value is supported.
StatusPurpose = "statusPurpose"

StatusPurposeRevocation = "revocation"
StatusPurposeSuspension = "suspension"
StatusPurposeMessage = "statusMessage"

StatusMessage = "statusMessage"
StatusSize = "statusSize"
StatusReference = "statusReference"
)

type credentialSubject struct {
ID string `json:"id"`
Type string `json:"type"`
Expand All @@ -24,7 +46,7 @@ func toVerifiableSubject(subject credentialSubject) []verifiable.Subject {
},
}
if subject.StatusPurpose != "" {
vcSub.CustomFields["statusPurpose"] = subject.StatusPurpose
vcSub.CustomFields[StatusPurpose] = subject.StatusPurpose
}

return []verifiable.Subject{vcSub}
Expand Down
5 changes: 3 additions & 2 deletions pkg/doc/vc/statustype/revocationlist2020.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,13 @@ func (s *revocationList2020Processor) ValidateStatus(vcStatus *verifiable.TypedI
}

// CreateVCStatus creates verifiable.TypedID.
func (s *revocationList2020Processor) CreateVCStatus(revocationListIndex, vcID string) *verifiable.TypedID {
func (s *revocationList2020Processor) CreateVCStatus(index, vcID, _ string,
_ ...vcapi.Field) *verifiable.TypedID {
return &verifiable.TypedID{
ID: uuid.New().URN(),
Type: string(vcapi.RevocationList2020VCStatus),
CustomFields: verifiable.CustomFields{
RevocationListIndex: revocationListIndex,
RevocationListIndex: index,
RevocationListCredential: vcID,
},
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/doc/vc/statustype/revocationlist2020_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func Test_revocationList2020Processor_CreateVC(t *testing.T) {

func Test_revocationList2020Processor_CreateVCStatus(t *testing.T) {
s := NewRevocationList2020Processor()
statusID := s.CreateVCStatus("1", "vcID2")
statusID := s.CreateVCStatus("1", "vcID2", "")

require.Equal(t, string(vcapi.RevocationList2020VCStatus), statusID.Type)
require.Equal(t, verifiable.CustomFields{
Expand Down
5 changes: 3 additions & 2 deletions pkg/doc/vc/statustype/revocationlist2021.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ func (s *revocationList2021Processor) ValidateStatus(vcStatus *verifiable.TypedI

// CreateVCStatus creates verifiable.TypedID.
// Doc: https://github.com/w3c-ccg/vc-status-list-2021/releases/tag/v0.0.1
func (s *revocationList2021Processor) CreateVCStatus(statusListIndex, vcID string) *verifiable.TypedID {
func (s *revocationList2021Processor) CreateVCStatus(index, vcID, _ string,
_ ...vcapi.Field) *verifiable.TypedID {
return &verifiable.TypedID{
ID: uuid.New().URN(),
Type: string(vcapi.RevocationList2021VCStatus),
CustomFields: verifiable.CustomFields{
StatusListIndex: statusListIndex,
StatusListIndex: index,
StatusListCredential: vcID,
},
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/doc/vc/statustype/revocationlist2021_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func Test_revocationList2021Processor_CreateVC(t *testing.T) {

func Test_revocationList2021Processor_CreateVCStatus(t *testing.T) {
s := NewRevocationList2021Processor()
statusID := s.CreateVCStatus("1", "vcID2")
statusID := s.CreateVCStatus("1", "vcID2", "")

require.Equal(t, string(vcapi.RevocationList2021VCStatus), statusID.Type)
require.Equal(t, verifiable.CustomFields{
Expand Down
Loading

0 comments on commit 3919504

Please sign in to comment.