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 9922c2b
Show file tree
Hide file tree
Showing 21 changed files with 946 additions and 638 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
6 changes: 5 additions & 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 Expand Up @@ -290,6 +290,10 @@ func (s *Manager) createAndStoreVC(ctx context.Context, signer *vc.Signer, cslUR
return fmt.Errorf("failed to marshal VC: %w", err)
}

fmt.Println("----------------- Status List VC -----------------")

Check failure on line 293 in pkg/cslmanager/cslmanager.go

View workflow job for this annotation

GitHub Actions / Checks

use of `fmt.Println` forbidden by pattern `^(fmt\.Print(|f|ln)|print|println)$` (forbidigo)
fmt.Println(string(vcBytes))

Check failure on line 294 in pkg/cslmanager/cslmanager.go

View workflow job for this annotation

GitHub Actions / Checks

use of `fmt.Println` forbidden by pattern `^(fmt\.Print(|f|ln)|print|println)$` (forbidigo)
fmt.Println("--------------------------------------------------")

Check failure on line 295 in pkg/cslmanager/cslmanager.go

View workflow job for this annotation

GitHub Actions / Checks

use of `fmt.Println` forbidden by pattern `^(fmt\.Print(|f|ln)|print|println)$` (forbidigo)

vcWrapper := &credentialstatus.CSLVCWrapper{
VCByte: vcBytes,
VC: vc,
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
8 changes: 7 additions & 1 deletion pkg/doc/vc/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0
package vc

import (
"github.com/samber/lo"
"github.com/trustbloc/vc-go/verifiable"
)

Expand All @@ -29,6 +30,11 @@ 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"
)

// StatusProcessor holds the list of methods required for processing different versions of Status(Revocation) List VC.
Expand All @@ -37,7 +43,7 @@ type StatusProcessor interface {
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 string, vcID string, purpose string, additionalFields ...lo.Tuple2[string, any]) *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 @@ -12,6 +12,7 @@ import (
"time"

"github.com/google/uuid"
"github.com/samber/lo"
utiltime "github.com/trustbloc/did-go/doc/util/time"
"github.com/trustbloc/vc-go/verifiable"

Expand Down Expand Up @@ -86,12 +87,12 @@ 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 string, vcID string, purpose string, additionalFields ...lo.Tuple2[string, any]) *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 @@ -12,6 +12,7 @@ import (
"time"

"github.com/google/uuid"
"github.com/samber/lo"
utiltime "github.com/trustbloc/did-go/doc/util/time"
"github.com/trustbloc/vc-go/verifiable"

Expand Down Expand Up @@ -90,12 +91,12 @@ 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 string, vcID string, purpose string, additionalFields ...lo.Tuple2[string, any]) *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 9922c2b

Please sign in to comment.