Skip to content

Commit

Permalink
feat: BitstringStatusList for VC model 2.0
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 committed Sep 24, 2024
1 parent b6ca73e commit be57b5d
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 be57b5d

Please sign in to comment.