From 5b70ee2a2a8a1b70516f34a4e50bb2ad7515ed5f Mon Sep 17 00:00:00 2001 From: Bob Stasyszyn Date: Fri, 22 Nov 2024 11:49:40 -0500 Subject: [PATCH] fix: Change encoding for BitstringStatusList encodedList to multibase base64url Signed-off-by: Bob Stasyszyn --- .../credentialstatus_service_test.go | 12 +- component/credentialstatus/go.mod | 2 +- go.mod | 2 +- pkg/doc/vc/bitstring/bitstring.go | 78 +++++++-- pkg/doc/vc/bitstring/bitstring_test.go | 52 ++++++ pkg/doc/vc/status.go | 2 + pkg/doc/vc/statustype/revocationlist2020.go | 10 +- pkg/doc/vc/statustype/revocationlist2021.go | 10 +- pkg/doc/vc/statustype/statuslist2021.go | 10 +- pkg/doc/vc/statustype/statuslist_bitstring.go | 14 +- .../statustype/statuslist_bitstring_test.go | 43 ++++- pkg/doc/vc/statustype/statusprocessor.go | 115 ++++++++++++- pkg/doc/vc/statustype/statusprocessor_test.go | 156 ++++++++++++++++++ pkg/internal/mock/status/status.go | 10 ++ .../eventhandler/eventhandler_service.go | 18 +- .../eventhandler/eventhandler_service_test.go | 2 +- .../verifycredential_service.go | 15 +- .../verifycredential_service_test.go | 70 +------- .../vc_v1_issue_verify_revoke_api.feature | 1 + test/bdd/testdata/crude_product_vcdm2.json | 99 +++++++++++ 20 files changed, 597 insertions(+), 124 deletions(-) create mode 100644 test/bdd/testdata/crude_product_vcdm2.json diff --git a/component/credentialstatus/credentialstatus_service_test.go b/component/credentialstatus/credentialstatus_service_test.go index 3aab112a7..fd64b5621 100644 --- a/component/credentialstatus/credentialstatus_service_test.go +++ b/component/credentialstatus/credentialstatus_service_test.go @@ -21,20 +21,19 @@ import ( "github.com/golang/mock/gomock" "github.com/google/uuid" + "github.com/multiformats/go-multibase" "github.com/piprate/json-gold/ld" "github.com/stretchr/testify/require" - "github.com/trustbloc/kms-go/spi/kms" - longform "github.com/trustbloc/sidetree-go/pkg/vdr/sidetreelongform" - - "github.com/trustbloc/vcs/internal/mock/vcskms" - timeutil "github.com/trustbloc/did-go/doc/util/time" vdr2 "github.com/trustbloc/did-go/vdr" vdr "github.com/trustbloc/did-go/vdr/api" vdrmock "github.com/trustbloc/did-go/vdr/mock" + "github.com/trustbloc/kms-go/spi/kms" + longform "github.com/trustbloc/sidetree-go/pkg/vdr/sidetreelongform" "github.com/trustbloc/vc-go/verifiable" "github.com/trustbloc/vcs/component/credentialstatus/internal/testutil" + "github.com/trustbloc/vcs/internal/mock/vcskms" "github.com/trustbloc/vcs/pkg/cslmanager" "github.com/trustbloc/vcs/pkg/doc/vc" "github.com/trustbloc/vcs/pkg/doc/vc/bitstring" @@ -121,7 +120,8 @@ func validateBitstringStatusListEntry( 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)) + bitString, err := bitstring.DecodeBits(credSubject[0].CustomFields["encodedList"].(string), + bitstring.WithMultibaseEncoding(multibase.Base64url)) require.NoError(t, err) revocationListIndex, err := strconv.Atoi(statusID.TypedID.CustomFields[statustype.StatusListIndex].(string)) diff --git a/component/credentialstatus/go.mod b/component/credentialstatus/go.mod index 7a5f3a0c9..ab156ec0d 100644 --- a/component/credentialstatus/go.mod +++ b/component/credentialstatus/go.mod @@ -11,6 +11,7 @@ toolchain go1.22.4 require ( github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 + github.com/multiformats/go-multibase v0.2.0 github.com/piprate/json-gold v0.5.1-0.20230111113000-6ddbe6e6f19f github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 @@ -94,7 +95,6 @@ require ( github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.0.14 // indirect github.com/multiformats/go-varint v0.0.6 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect diff --git a/go.mod b/go.mod index 88b3f6bdb..8a7f266f2 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/jinzhu/copier v0.3.5 github.com/klauspost/compress v1.17.9 github.com/labstack/echo/v4 v4.12.0 + github.com/multiformats/go-multibase v0.2.0 github.com/ory/dockertest/v3 v3.10.1-0.20240704115616-d229e74b748d github.com/ory/fosite v0.47.0 github.com/ory/x v0.0.655 @@ -179,7 +180,6 @@ require ( github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multibase v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect diff --git a/pkg/doc/vc/bitstring/bitstring.go b/pkg/doc/vc/bitstring/bitstring.go index 13793cdb4..b957ddbbe 100644 --- a/pkg/doc/vc/bitstring/bitstring.go +++ b/pkg/doc/vc/bitstring/bitstring.go @@ -11,6 +11,8 @@ import ( "compress/gzip" "encoding/base64" "fmt" + + "github.com/multiformats/go-multibase" ) const ( @@ -20,21 +22,70 @@ const ( // BitString struct. type BitString struct { - bits []byte - numBits int + bits []byte + numBits int + multibaseEncoding multibase.Encoding +} + +type Opt func(*options) + +type options struct { + multibaseEncoding multibase.Encoding +} + +// WithMultibaseEncoding sets the multibase encoding. +func WithMultibaseEncoding(value multibase.Encoding) Opt { + return func(options *options) { + options.multibaseEncoding = value + } } // NewBitString return bitstring. -func NewBitString(length int) *BitString { +func NewBitString(length int, opts ...Opt) *BitString { + options := &options{} + + for _, opt := range opts { + opt(options) + } + size := 1 + ((length - 1) / bitsPerByte) - return &BitString{bits: make([]byte, size), numBits: length} + + return &BitString{ + bits: make([]byte, size), + numBits: length, + multibaseEncoding: options.multibaseEncoding, + } } // DecodeBits decode bits. -func DecodeBits(encodedBits string) (*BitString, error) { - decodedBits, err := base64.RawURLEncoding.DecodeString(encodedBits) - if err != nil { - return nil, err +func DecodeBits(encodedBits string, opts ...Opt) (*BitString, error) { + options := &options{} + + for _, opt := range opts { + opt(options) + } + + var decodedBits []byte + + if options.multibaseEncoding != multibase.Encoding(0) { + var encoding multibase.Encoding + var err error + + encoding, decodedBits, err = multibase.Decode(encodedBits) + if err != nil { + return nil, err + } + + if encoding != options.multibaseEncoding { + return nil, fmt.Errorf("encoding not supported: %d", encoding) + } + } else { + var err error + + decodedBits, err = base64.RawURLEncoding.DecodeString(encodedBits) + if err != nil { + return nil, err + } } b := bytes.NewReader(decodedBits) @@ -49,7 +100,10 @@ func DecodeBits(encodedBits string) (*BitString, error) { return nil, err } - return &BitString{bits: buf.Bytes()}, nil + return &BitString{ + bits: buf.Bytes(), + multibaseEncoding: options.multibaseEncoding, + }, nil } // Set bit. @@ -99,5 +153,9 @@ func (b *BitString) EncodeBits() (string, error) { return "", err } - return base64.RawURLEncoding.EncodeToString(buf.Bytes()), nil + if b.multibaseEncoding == multibase.Encoding(0) { + return base64.RawURLEncoding.EncodeToString(buf.Bytes()), nil + } + + return multibase.Encode(b.multibaseEncoding, buf.Bytes()) } diff --git a/pkg/doc/vc/bitstring/bitstring_test.go b/pkg/doc/vc/bitstring/bitstring_test.go index d86cdbe8e..64de95b3a 100644 --- a/pkg/doc/vc/bitstring/bitstring_test.go +++ b/pkg/doc/vc/bitstring/bitstring_test.go @@ -9,6 +9,7 @@ package bitstring import ( "testing" + "github.com/multiformats/go-multibase" "github.com/stretchr/testify/require" ) @@ -31,6 +32,21 @@ func TestBitString(t *testing.T) { require.Contains(t, err.Error(), "illegal base64 data at input") }) + t.Run("invalid multibase string", func(t *testing.T) { + _, err := DecodeBits("!!!!wrongvalue", WithMultibaseEncoding(multibase.Base64url)) + require.Error(t, err) + require.Contains(t, err.Error(), "selected encoding not supported") + }) + + t.Run("unsupported multibase encoding", func(t *testing.T) { + str, err := multibase.Encode(multibase.Base64pad, []byte("data")) + require.NoError(t, err) + + _, err = DecodeBits(str, WithMultibaseEncoding(multibase.Base64url)) + require.Error(t, err) + require.Contains(t, err.Error(), "encoding not supported") + }) + t.Run("test success", func(t *testing.T) { bitString := NewBitString(17) @@ -66,4 +82,40 @@ func TestBitString(t *testing.T) { require.NoError(t, err) require.False(t, bitSet) }) + + t.Run("Multibase-encoding success", func(t *testing.T) { + bitString := NewBitString(17, WithMultibaseEncoding(multibase.Base64url)) + + err := bitString.Set(1, true) + require.NoError(t, err) + + bitSet, err := bitString.Get(1) + require.NoError(t, err) + require.True(t, bitSet) + + bitSet, err = bitString.Get(0) + require.NoError(t, err) + require.False(t, bitSet) + + encodeBits, err := bitString.EncodeBits() + require.NoError(t, err) + + bitStr, err := DecodeBits(encodeBits, WithMultibaseEncoding(multibase.Base64url)) + require.NoError(t, err) + + bitSet, err = bitStr.Get(1) + require.NoError(t, err) + require.True(t, bitSet) + + bitSet, err = bitStr.Get(0) + require.NoError(t, err) + require.False(t, bitSet) + + err = bitStr.Set(1, false) + require.NoError(t, err) + + bitSet, err = bitStr.Get(1) + require.NoError(t, err) + require.False(t, bitSet) + }) } diff --git a/pkg/doc/vc/status.go b/pkg/doc/vc/status.go index 46982a867..49dfbee2a 100644 --- a/pkg/doc/vc/status.go +++ b/pkg/doc/vc/status.go @@ -50,6 +50,8 @@ type StatusProcessor interface { CreateVC(vcID string, listSize int, profile *Signer) (*verifiable.Credential, error) CreateVCStatus(index, vcID, purpose string, additionalFields ...Field) *verifiable.TypedID GetVCContext() string + UpdateStatus(vc *verifiable.Credential, status bool, indexes ...int) (*verifiable.Credential, error) + IsSet(vc *verifiable.Credential, index int) (bool, error) } type StatusProcessorGetter func(vcStatusListType StatusType) (StatusProcessor, error) diff --git a/pkg/doc/vc/statustype/revocationlist2020.go b/pkg/doc/vc/statustype/revocationlist2020.go index 108722c97..0dbcc92b7 100644 --- a/pkg/doc/vc/statustype/revocationlist2020.go +++ b/pkg/doc/vc/statustype/revocationlist2020.go @@ -37,11 +37,17 @@ const ( // revocationList2020Processor implements Revocation List 2020. // Spec: https://w3c-ccg.github.io/vc-status-rl-2020/ -type revocationList2020Processor struct{} +type revocationList2020Processor struct { + *statusListProcessor +} // NewRevocationList2020Processor returns new revocationList2020Processor. func NewRevocationList2020Processor() *revocationList2020Processor { //nolint:revive - return &revocationList2020Processor{} + return &revocationList2020Processor{ + statusListProcessor: &statusListProcessor{ + statusType: revocationList2020VCSubjectType, + }, + } } // GetStatusVCURI returns the ID (URL) of status VC. diff --git a/pkg/doc/vc/statustype/revocationlist2021.go b/pkg/doc/vc/statustype/revocationlist2021.go index e6d813da7..86f6bdb3d 100644 --- a/pkg/doc/vc/statustype/revocationlist2021.go +++ b/pkg/doc/vc/statustype/revocationlist2021.go @@ -31,11 +31,17 @@ const ( // revocationList2021Processor implements version 0.0.1 of Status list 2021. // Release: https://github.com/w3c-ccg/vc-status-list-2021/releases/tag/v0.0.1 -type revocationList2021Processor struct{} +type revocationList2021Processor struct { + *statusListProcessor +} // NewRevocationList2021Processor returns new revocationList2021Processor. func NewRevocationList2021Processor() *revocationList2021Processor { //nolint:revive - return &revocationList2021Processor{} + return &revocationList2021Processor{ + statusListProcessor: &statusListProcessor{ + statusType: revocationList2021VCSubjectType, + }, + } } // GetStatusVCURI returns the ID (URL) of status VC. diff --git a/pkg/doc/vc/statustype/statuslist2021.go b/pkg/doc/vc/statustype/statuslist2021.go index 5a752d35d..d9f6c423c 100644 --- a/pkg/doc/vc/statustype/statuslist2021.go +++ b/pkg/doc/vc/statustype/statuslist2021.go @@ -35,11 +35,17 @@ const ( // statusList2021Processor implements f Status List 2021. // Spec: https://w3c-ccg.github.io/vc-status-list-2021/#statuslist2021credential -type statusList2021Processor struct{} +type statusList2021Processor struct { + *statusListProcessor +} // NewStatusList2021Processor returns new statusList2021Processor. func NewStatusList2021Processor() *statusList2021Processor { //nolint:revive - return &statusList2021Processor{} + return &statusList2021Processor{ + statusListProcessor: &statusListProcessor{ + statusType: StatusList2021VCSubjectType, + }, + } } // GetStatusVCURI returns the ID (URL) of status VC. diff --git a/pkg/doc/vc/statustype/statuslist_bitstring.go b/pkg/doc/vc/statustype/statuslist_bitstring.go index 4df1cf111..2c2248fb8 100644 --- a/pkg/doc/vc/statustype/statuslist_bitstring.go +++ b/pkg/doc/vc/statustype/statuslist_bitstring.go @@ -14,6 +14,7 @@ import ( "time" "github.com/google/uuid" + "github.com/multiformats/go-multibase" utiltime "github.com/trustbloc/did-go/doc/util/time" "github.com/trustbloc/vc-go/verifiable" @@ -38,11 +39,18 @@ const ( // BitstringStatusListProcessor implements the Bitstring Status List Entry. // Spec: https://www.w3.org/TR/vc-bitstring-status-list/ -type BitstringStatusListProcessor struct{} +type BitstringStatusListProcessor struct { + *statusListProcessor +} // NewBitstringStatusListProcessor returns new BitstringStatusListProcessor. func NewBitstringStatusListProcessor() *BitstringStatusListProcessor { - return &BitstringStatusListProcessor{} + return &BitstringStatusListProcessor{ + statusListProcessor: &statusListProcessor{ + statusType: StatusListBitstringVCSubjectType, + multibaseEncoding: multibase.Base64url, + }, + } } // GetStatusVCURI returns the ID (URL) of status VC. @@ -177,7 +185,7 @@ func (s *BitstringStatusListProcessor) CreateVC(vcID string, listSize int, size = bitStringSize } - encodeBits, err := bitstring.NewBitString(size).EncodeBits() + encodeBits, err := bitstring.NewBitString(size, bitstring.WithMultibaseEncoding(multibase.Base64url)).EncodeBits() if err != nil { return nil, err } diff --git a/pkg/doc/vc/statustype/statuslist_bitstring_test.go b/pkg/doc/vc/statustype/statuslist_bitstring_test.go index 597295f3b..0f0098325 100644 --- a/pkg/doc/vc/statustype/statuslist_bitstring_test.go +++ b/pkg/doc/vc/statustype/statuslist_bitstring_test.go @@ -9,6 +9,7 @@ package statustype import ( "testing" + "github.com/multiformats/go-multibase" "github.com/stretchr/testify/require" "github.com/trustbloc/vc-go/verifiable" @@ -281,7 +282,8 @@ func Test_BitstringStatusListProcessor_CreateVC(t *testing.T) { "https://w3id.org/security/suites/ed25519-2018/v1"}, vcc.Context) require.Equal(t, []string{vcType, StatusListBitstringVCType}, vcc.Types) require.Equal(t, &verifiable.Issuer{ID: "did:example:123"}, vcc.Issuer) - encodeBits, err := bitstring.NewBitString(bitStringSize).EncodeBits() + encodeBits, err := bitstring.NewBitString(bitStringSize, + bitstring.WithMultibaseEncoding(multibase.Base64url)).EncodeBits() require.NotEmpty(t, vc.ToRawClaimsMap()["validFrom"]) require.NoError(t, err) require.Equal(t, []verifiable.Subject{{ @@ -402,3 +404,42 @@ func Test_BitstringStatusListProcessor_GetVCContext(t *testing.T) { require.Equal(t, "https://www.w3.org/ns/credentials/v2", s.GetVCContext()) } + +func Test_BitstringStatusList_IsSet(t *testing.T) { + vc, err := verifiable.ParseCredential([]byte(bitstringCSLVC), + verifiable.WithCredDisableValidation(), + verifiable.WithDisabledProofCheck(), + ) + require.NoError(t, err) + + s := NewBitstringStatusListProcessor() + + set, err := s.IsSet(vc, 1000) + require.NoError(t, err) + require.False(t, set) +} + +const bitstringCSLVC = `{ + "@context": [ + "https://www.w3.org/ns/credentials/v2" + ], + "id": "https://dhs-svip.github.io/ns/uscis/status/3", + "type": [ + "VerifiableCredential", + "BitstringStatusListCredential" + ], + "credentialSubject": { + "id": "https://dhs-svip.github.io/ns/uscis/status/3#list", + "type": "BitstringStatusList", + "encodedList": "uH4sIAAAAAAAAA-3OMQEAAAgDoEU3ugEWwENIQMI3cx0AAAAAAAAAAAAAAAAAAACgLGiNcIEAQAAA", + "statusPurpose": "revocation" + }, + "issuer": "did:web:dhs-svip.github.io:ns:uscis:oidp", + "proof": { + "type": "DataIntegrityProof", + "verificationMethod": "did:web:dhs-svip.github.io:ns:uscis:oidp#zDnaekqKLkVN1HqzBxy1Ti8niyCRxWkKr6cxDvX6P4qXDBATd", + "cryptosuite": "ecdsa-rdfc-2019", + "proofPurpose": "assertionMethod", + "proofValue": "zLLLMLuL6feiYZ1vDU7AVaJGRpmbbi1bf8Xv9JL15sW6aZTVzrfJqb9UFPWmgPD3Mnk5C3EpN3eKvzC27fdVM3Y6" + } +}` diff --git a/pkg/doc/vc/statustype/statusprocessor.go b/pkg/doc/vc/statustype/statusprocessor.go index 128253e4d..7ff7875dc 100644 --- a/pkg/doc/vc/statustype/statusprocessor.go +++ b/pkg/doc/vc/statustype/statusprocessor.go @@ -9,21 +9,128 @@ package statustype import ( "fmt" + "github.com/multiformats/go-multibase" + "github.com/trustbloc/vc-go/verifiable" + vcapi "github.com/trustbloc/vcs/pkg/doc/vc" + "github.com/trustbloc/vcs/pkg/doc/vc/bitstring" +) + +const ( + jsonFieldStatusListType = "type" + jsonFieldEncodedList = "encodedList" ) // GetVCStatusProcessor returns statustype.StatusProcessor. func GetVCStatusProcessor(vcStatusListType vcapi.StatusType) (vcapi.StatusProcessor, error) { switch vcStatusListType { - case vcapi.StatusList2021VCStatus: + case vcapi.StatusList2021VCStatus, StatusList2021VCSubjectType: return NewStatusList2021Processor(), nil - case vcapi.BitstringStatusList: + case vcapi.BitstringStatusList, StatusListBitstringVCSubjectType: return NewBitstringStatusListProcessor(), nil - case vcapi.RevocationList2021VCStatus: + case vcapi.RevocationList2021VCStatus, revocationList2021VCSubjectType: return NewRevocationList2021Processor(), nil - case vcapi.RevocationList2020VCStatus: + case vcapi.RevocationList2020VCStatus, revocationList2020VCSubjectType: return NewRevocationList2020Processor(), nil default: return nil, fmt.Errorf("unsupported VCStatusListType %s", vcStatusListType) } } + +type statusListProcessor struct { + statusType vcapi.StatusType + multibaseEncoding multibase.Encoding +} + +func (p *statusListProcessor) UpdateStatus( + vc *verifiable.Credential, + status bool, + indexes ...int, +) (*verifiable.Credential, error) { + credSubject := vc.Contents().Subject + + if err := p.validateSubject(credSubject); err != nil { + return nil, err + } + + bitString, err := p.getBitstring(&credSubject[0]) + if err != nil { + return nil, err + } + + for _, index := range indexes { + if errSet := bitString.Set(index, status); errSet != nil { + return nil, fmt.Errorf("bitString.Set failed: %w", errSet) + } + } + + credSubject[0].CustomFields[jsonFieldEncodedList], err = bitString.EncodeBits() + if err != nil { + return nil, fmt.Errorf("bitString.EncodeBits failed: %w", err) + } + + return vc.WithModifiedSubject(credSubject), nil +} + +func (p *statusListProcessor) IsSet(vc *verifiable.Credential, index int) (bool, error) { + credSubject := vc.Contents().Subject + + if err := p.validateSubject(credSubject); err != nil { + return false, err + } + + bitString, err := p.getBitstring(&credSubject[0]) + if err != nil { + return false, err + } + + bitSet, err := bitString.Get(index) + if err != nil { + return false, fmt.Errorf("failed to get bit: %w", err) + } + + return bitSet, nil +} + +func (p *statusListProcessor) getBitstring(subject *verifiable.Subject) (*bitstring.BitString, error) { + encodedList, err := getStringValue(jsonFieldEncodedList, subject.CustomFields) + if err != nil { + return nil, fmt.Errorf("failed to get encodedList: %w", err) + } + + bitString, err := bitstring.DecodeBits(encodedList, bitstring.WithMultibaseEncoding(p.multibaseEncoding)) + if err != nil { + return nil, fmt.Errorf("failed to decode encodedList: %w", err) + } + + return bitString, nil +} + +func (p *statusListProcessor) validateSubject(subject []verifiable.Subject) error { + if len(subject) == 0 { + return fmt.Errorf("invalid subject field structure") + } + + statusType, err := getStringValue(jsonFieldStatusListType, subject[0].CustomFields) + if err != nil { + return fmt.Errorf("failed to get status list type: %w", err) + } + + if vcapi.StatusType(statusType) != p.statusType { + return fmt.Errorf("unsupported status list type: %s", statusType) + } + + return nil +} + +func getStringValue(key string, vMap map[string]interface{}) (string, error) { + if val, ok := vMap[key]; ok { + if s, ok := val.(string); ok { + return s, nil + } + + return "", fmt.Errorf("invalid '%s' type", key) + } + + return "", nil +} diff --git a/pkg/doc/vc/statustype/statusprocessor_test.go b/pkg/doc/vc/statustype/statusprocessor_test.go index ac3625e54..19314945c 100644 --- a/pkg/doc/vc/statustype/statusprocessor_test.go +++ b/pkg/doc/vc/statustype/statusprocessor_test.go @@ -9,10 +9,12 @@ package statustype import ( "testing" + "github.com/multiformats/go-multibase" "github.com/stretchr/testify/require" "github.com/trustbloc/vc-go/verifiable" vcapi "github.com/trustbloc/vcs/pkg/doc/vc" + vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" ) func TestGetVCStatusProcessor_StatusList2021VCStatus(t *testing.T) { @@ -49,3 +51,157 @@ func TestGetVCStatusProcessor_UnsupportedVCStatusListType(t *testing.T) { require.Nil(t, processor) require.Contains(t, err.Error(), "unsupported VCStatusListType") } + +func TestStatusListProcessor(t *testing.T) { + s := NewBitstringStatusListProcessor() + vc, err := s.CreateVC("vcID1", 10, &vcapi.Signer{ + DID: "did:example:123", + SignatureType: vcsverifiable.Ed25519Signature2018, + }) + require.NoError(t, err) + + t.Run("success", func(t *testing.T) { + processor := &statusListProcessor{ + statusType: StatusListBitstringVCSubjectType, + multibaseEncoding: multibase.Base64url, + } + + set, e := processor.IsSet(vc, 1) + require.NoError(t, e) + require.False(t, set) + + vc, e = processor.UpdateStatus(vc, true, 1) + require.NoError(t, e) + + set, e = processor.IsSet(vc, 1) + require.NoError(t, e) + require.True(t, set) + }) + + t.Run("invalid status type -> error", func(t *testing.T) { + processor := &statusListProcessor{ + statusType: StatusList2021VCSubjectType, + multibaseEncoding: multibase.Base64url, + } + + _, err := processor.IsSet(vc, 1) + require.ErrorContains(t, err, "unsupported status list type") + }) + + t.Run("non-multibase decoding -> error", func(t *testing.T) { + processor := &statusListProcessor{ + statusType: StatusListBitstringVCSubjectType, + } + + _, e := processor.IsSet(vc, 1) + require.ErrorContains(t, e, "failed to decode encodedList") + }) + + t.Run("invalid multibase encoding -> error", func(t *testing.T) { + processor := &statusListProcessor{ + statusType: StatusListBitstringVCSubjectType, + multibaseEncoding: multibase.Base64urlPad, + } + + _, e := processor.IsSet(vc, 1) + require.ErrorContains(t, e, "failed to decode encodedList") + }) + + t.Run("no subject -> error", func(t *testing.T) { + processor := &statusListProcessor{ + statusType: StatusListBitstringVCSubjectType, + multibaseEncoding: multibase.Base64url, + } + + _, e := processor.IsSet(&verifiable.Credential{}, 1) + require.ErrorContains(t, e, "invalid subject field structure") + }) + + t.Run("invalid subject type -> error", func(t *testing.T) { + processor := &statusListProcessor{ + statusType: StatusListBitstringVCSubjectType, + multibaseEncoding: multibase.Base64url, + } + + invalidVC, e := verifiable.ParseCredential([]byte(invalidTypeCSLVC), + verifiable.WithCredDisableValidation(), verifiable.WithDisabledProofCheck()) + require.NoError(t, e) + + _, e = processor.IsSet(invalidVC, 1) + require.ErrorContains(t, e, "failed to get status list type: invalid 'type' type") + }) + + t.Run("invalid subject encodedList -> error", func(t *testing.T) { + processor := &statusListProcessor{ + statusType: StatusListBitstringVCSubjectType, + multibaseEncoding: multibase.Base64url, + } + + invalidVC, e := verifiable.ParseCredential([]byte(invalidEncodedListCSLVC), + verifiable.WithCredDisableValidation(), verifiable.WithDisabledProofCheck()) + require.NoError(t, e) + + _, e = processor.IsSet(invalidVC, 1) + require.ErrorContains(t, e, "failed to get encodedList: invalid 'encodedList' type") + }) + + t.Run("updateStatus - invalid status type -> error", func(t *testing.T) { + processor := &statusListProcessor{ + statusType: StatusList2021VCSubjectType, + multibaseEncoding: multibase.Base64url, + } + + _, e := processor.UpdateStatus(vc, true, 1) + require.ErrorContains(t, e, "unsupported status list type") + }) + + t.Run("updateStatus - invalid subject encodedList -> error", func(t *testing.T) { + processor := &statusListProcessor{ + statusType: StatusListBitstringVCSubjectType, + multibaseEncoding: multibase.Base64url, + } + + invalidVC, e := verifiable.ParseCredential([]byte(invalidEncodedListCSLVC), + verifiable.WithCredDisableValidation(), verifiable.WithDisabledProofCheck()) + require.NoError(t, e) + + _, e = processor.UpdateStatus(invalidVC, true, 1) + require.ErrorContains(t, e, "failed to get encodedList: invalid 'encodedList' type") + }) +} + +const invalidEncodedListCSLVC = `{ + "@context": [ + "https://www.w3.org/ns/credentials/v2" + ], + "credentialSubject": { + "encodedList": 123, + "id": "did:web:example.com:12345#list", + "statusPurpose": "revocation", + "type": "BitstringStatusList" + }, + "id": "did:web:example.com:12345", + "issuer": "did:test:abc", + "type": [ + "VerifiableCredential", + "BitstringStatusListCredential" + ] +}` + +const invalidTypeCSLVC = `{ + "@context": [ + "https://www.w3.org/ns/credentials/v2" + ], + "credentialSubject": { + "encodedList": "uH4sIAAAAAAAA_-zAgQAAAACAoP2pF6kAAAAAAAAAAAAAAAAAAACgOgAA__-N53xXgD4AAA", + "id": "did:web:example.com:12345#list", + "statusPurpose": "revocation", + "type": 123 + }, + "id": "did:web:example.com:12345", + "issuer": "did:test:abc", + "type": [ + "VerifiableCredential", + "BitstringStatusListCredential" + ] +}` diff --git a/pkg/internal/mock/status/status.go b/pkg/internal/mock/status/status.go index 34c481571..bb5c7dac3 100644 --- a/pkg/internal/mock/status/status.go +++ b/pkg/internal/mock/status/status.go @@ -31,6 +31,8 @@ type MockVCStatusProcessor struct { CreateVCErr error VCStatus *verifiable.TypedID VCContext string + Set bool + IsSetErr error } func (m *MockVCStatusProcessor) ValidateStatus(_ *verifiable.TypedID) error { @@ -56,3 +58,11 @@ func (m *MockVCStatusProcessor) CreateVCStatus(string, string, string, ...vc.Fie func (m *MockVCStatusProcessor) GetVCContext() string { return m.VCContext } + +func (m *MockVCStatusProcessor) UpdateStatus(*verifiable.Credential, bool, ...int) (*verifiable.Credential, error) { + panic("not implemented") +} + +func (m *MockVCStatusProcessor) IsSet(*verifiable.Credential, int) (bool, error) { + return m.Set, m.IsSetErr +} diff --git a/pkg/service/credentialstatus/eventhandler/eventhandler_service.go b/pkg/service/credentialstatus/eventhandler/eventhandler_service.go index 709523d0e..9fb267b39 100644 --- a/pkg/service/credentialstatus/eventhandler/eventhandler_service.go +++ b/pkg/service/credentialstatus/eventhandler/eventhandler_service.go @@ -19,8 +19,8 @@ import ( "github.com/trustbloc/vcs/internal/logfields" "github.com/trustbloc/vcs/pkg/doc/vc" - "github.com/trustbloc/vcs/pkg/doc/vc/bitstring" vccrypto "github.com/trustbloc/vcs/pkg/doc/vc/crypto" + "github.com/trustbloc/vcs/pkg/doc/vc/statustype" vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" "github.com/trustbloc/vcs/pkg/event/spi" vcskms "github.com/trustbloc/vcs/pkg/kms" @@ -35,6 +35,7 @@ const ( jsonKeyProofPurpose = "proofPurpose" jsonKeyVerificationMethod = "verificationMethod" jsonKeySignatureOfType = "type" + jsonStatusListType = "type" ) var logger = log.New("credentialstatus-eventhandler") @@ -109,22 +110,21 @@ func (s *Service) handleEventPayload( cs := clsWrapper.VC.Contents().Subject - bitString, err := bitstring.DecodeBits(cs[0].CustomFields["encodedList"].(string)) + statusType, err := getStringValue(jsonStatusListType, cs[0].CustomFields) if err != nil { - return fmt.Errorf("get encodedList from CSL customFields failed: %w", err) + return fmt.Errorf("failed to get status list type: %w", err) } - if errSet := bitString.Set(payload.Index, payload.Status); errSet != nil { - return fmt.Errorf("bitString.Set failed: %w", errSet) + processor, err := statustype.GetVCStatusProcessor(vc.StatusType(statusType)) + if err != nil { + return fmt.Errorf("failed to get VCStatusProcessor: %w", err) } - cs[0].CustomFields["encodedList"], err = bitString.EncodeBits() + clsWrapper.VC, err = processor.UpdateStatus(clsWrapper.VC, payload.Status, payload.Index) if err != nil { - return fmt.Errorf("bitString.EncodeBits failed: %w", err) + return fmt.Errorf("failed to update status: %w", err) } - clsWrapper.VC = clsWrapper.VC.WithModifiedSubject(cs) - signedCredentialBytes, err := s.signCSL(payload.ProfileID, payload.ProfileVersion, clsWrapper.VC) if err != nil { return fmt.Errorf("failed to sign CSL: %w", err) diff --git a/pkg/service/credentialstatus/eventhandler/eventhandler_service_test.go b/pkg/service/credentialstatus/eventhandler/eventhandler_service_test.go index 0bf7bee9d..a1ac9e32d 100644 --- a/pkg/service/credentialstatus/eventhandler/eventhandler_service_test.go +++ b/pkg/service/credentialstatus/eventhandler/eventhandler_service_test.go @@ -321,7 +321,7 @@ func TestService_handleEventPayload(t *testing.T) { err = s.handleEventPayload(ctx, eventPayload) require.Error(t, err) - require.ErrorContains(t, err, "get encodedList from CSL customFields failed") + require.ErrorContains(t, err, "failed to update status: failed to decode encodedList") cslWrapper, err = cslStore.Get(ctx, cslURL) require.NoError(t, err) diff --git a/pkg/service/verifycredential/verifycredential_service.go b/pkg/service/verifycredential/verifycredential_service.go index 7f2d3fd40..8ace4d474 100644 --- a/pkg/service/verifycredential/verifycredential_service.go +++ b/pkg/service/verifycredential/verifycredential_service.go @@ -26,7 +26,6 @@ import ( "github.com/trustbloc/vcs/internal/logfields" "github.com/trustbloc/vcs/pkg/doc/vc" - "github.com/trustbloc/vcs/pkg/doc/vc/bitstring" "github.com/trustbloc/vcs/pkg/doc/vc/crypto" "github.com/trustbloc/vcs/pkg/internal/common/diddoc" profileapi "github.com/trustbloc/vcs/pkg/profile" @@ -264,19 +263,9 @@ func (s *Service) ValidateVCStatus(ctx context.Context, vcStatus *verifiable.Typ return fmt.Errorf("issuer of the credential do not match status list vc issuer") } - credSubject := statusListVCC.Subject - if len(credSubject) == 0 { - return fmt.Errorf("invalid subject field structure") - } - - bitString, err := bitstring.DecodeBits(credSubject[0].CustomFields["encodedList"].(string)) - if err != nil { - return fmt.Errorf("failed to decode bits: %w", err) - } - - bitSet, err := bitString.Get(statusListIndex) + bitSet, err := vcStatusProcessor.IsSet(statusListVC, statusListIndex) if err != nil { - return err + return fmt.Errorf("failed to check if bit is set: %w", err) } if bitSet { diff --git a/pkg/service/verifycredential/verifycredential_service_test.go b/pkg/service/verifycredential/verifycredential_service_test.go index 5d03e4627..a62793850 100644 --- a/pkg/service/verifycredential/verifycredential_service_test.go +++ b/pkg/service/verifycredential/verifycredential_service_test.go @@ -513,75 +513,6 @@ func TestService_checkVCStatus(t *testing.T) { }, wantErr: true, }, - { - name: "revocationListVC invalid subject field error", - fields: fields{ - getStatusListVCGetter: func() statusListVCURIResolver { - mockStatusListVCGetter := NewMockStatusListVCResolver(gomock.NewController(t)) - mockStatusListVCGetter.EXPECT().Resolve(context.Background(), gomock.Any()).AnyTimes().Return( - createVC(t, verifiable.CredentialContents{ - Subject: []verifiable.Subject{}, - Issuer: &verifiable.Issuer{ - ID: "did:trustblock:abc", - }, - }), nil) - - return mockStatusListVCGetter - }, - getVCStatusProcessorGetter: func() vc.StatusProcessorGetter { - mockStatusProcessorGetter := &status.MockStatusProcessorGetter{ - StatusProcessor: &status.MockVCStatusProcessor{}, - } - - return mockStatusProcessorGetter.GetMockStatusProcessor - }, - }, - args: args{ - getVcStatus: func() *verifiable.TypedID { - return validVCStatus - }, - issuer: &verifiable.Issuer{ID: "did:trustblock:abc"}, - }, - wantErr: true, - }, - { - name: "revocationListVC invalid encodedList field error", - fields: fields{ - getStatusListVCGetter: func() statusListVCURIResolver { - mockStatusListVCGetter := NewMockStatusListVCResolver(gomock.NewController(t)) - mockStatusListVCGetter.EXPECT().Resolve(context.Background(), gomock.Any()).AnyTimes().Return( - createVC(t, verifiable.CredentialContents{ - Subject: []verifiable.Subject{{ - ID: "", - CustomFields: map[string]interface{}{ - "statusListIndex": "1", - "statusPurpose": "2", - "encodedList": "", - }, - }}, - Issuer: &verifiable.Issuer{ - ID: "did:trustblock:abc", - }, - }), nil) - - return mockStatusListVCGetter - }, - getVCStatusProcessorGetter: func() vc.StatusProcessorGetter { - mockStatusProcessorGetter := &status.MockStatusProcessorGetter{ - StatusProcessor: &status.MockVCStatusProcessor{}, - } - - return mockStatusProcessorGetter.GetMockStatusProcessor - }, - }, - args: args{ - getVcStatus: func() *verifiable.TypedID { - return validVCStatus - }, - issuer: &verifiable.Issuer{ID: "did:trustblock:abc"}, - }, - wantErr: true, - }, { name: "revocationListVC bitString.Get() error", fields: fields{ @@ -608,6 +539,7 @@ func TestService_checkVCStatus(t *testing.T) { mockStatusProcessorGetter := &status.MockStatusProcessorGetter{ StatusProcessor: &status.MockVCStatusProcessor{ StatusListIndex: -1, + IsSetErr: errors.New("injected error"), }, } diff --git a/test/bdd/features/vc_v1_issue_verify_revoke_api.feature b/test/bdd/features/vc_v1_issue_verify_revoke_api.feature index 682a9b2a9..d3cc1132c 100644 --- a/test/bdd/features/vc_v1_issue_verify_revoke_api.feature +++ b/test/bdd/features/vc_v1_issue_verify_revoke_api.feature @@ -26,6 +26,7 @@ Feature: Using VC REST API | i_myprofile_ud_es256k_jwt/v1.0 | v_myprofile_jwt/v1.0 | permanent_resident_card.json | | i_myprofile_ud_es256k_sdjwt/v1.0 | v_myprofile_jwt/v1.0 | crude_product.json | | i_myprofile_ud_di_ecdsa-2019/v1.0 | v_myprofile_ldp/v1.0 | crude_product.json | + | i_myprofile_cmtr_p256_ldp_v2/v1.0 | v_myprofile_ldp/v1.0 | crude_product_vcdm2.json | @e2e_ldp_jwt_sdjwt_revoke_err Scenario Outline: Unsuccessful attempt to revoke credential from wrong issuer (LDP, JWT, SD-JWT). diff --git a/test/bdd/testdata/crude_product_vcdm2.json b/test/bdd/testdata/crude_product_vcdm2.json new file mode 100644 index 000000000..ff1c76685 --- /dev/null +++ b/test/bdd/testdata/crude_product_vcdm2.json @@ -0,0 +1,99 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://trustbloc.github.io/context/vc/examples-crude-product-v2.jsonld" + ], + "id": "http://neo-flow.com/credentials/3a185b8f-078a-4646-8343-76a45c2856a5", + "type": [ + "VerifiableCredential", + "CrudeProductCredential" + ], + "name": "Heavy Sour Dilbit", + "description": "Crude oil stream, produced from diluted bitumen.", + "issuer": "did:key:z6MkiTsvjrrPNDZ1rrg9QDEYCFWCmEswT6U2cEkScb7edQ9b", + "validFrom": "2020-05-01T00:45:04.789Z", + "credentialSubject": { + "id": "did:example:b34ca6cd37bbf23", + "producer": "did:v1:test:nym:z6MkfG5HTrBXzsAP8AbayNpG3ZaoyM4PCqNPrdWQRSpHDV6J", + "category": "Western Canadian Select", + "hsCode": "270900", + "identifier": "3a185b8f-078a-4646-8343-76a45c2856a5", + "name": "Heavy Sour Dilbit", + "description": "Crude oil stream, produced from diluted bitumen.", + "volume": "10000", + "address": { + "address": "Edmonton, CAN", + "latitude": "53.5461", + "longitude": "113.4938" + }, + "productionDate": "2020-03-30T07:23:14.206Z", + "predecessorOf": "c98f2452-ab18-4cbe-bf89-635fb8ae7f33", + "successorOf": "", + "physicalSpecs": { + "uom": "barrel", + "minimumQuantity": "1000", + "apiGravity": 21, + "viscosityAt10C": "302", + "viscosityAt20C": "157", + "viscosityAt30C": "89.6", + "viscosityAt40C": "55.3", + "viscosityAt45C": "44.4", + "pourPoint": "-30", + "vapourPressure": "51.7", + "density": "928", + "naphtha": "", + "distillateAt350To650F": "", + "gasOilAt650To980F": "", + "residAt980F": "41", + "deemedButane": "1.9", + "tan": "1.05", + "ron": "", + "mon": "", + "boilingPoint": "", + "freezingPoint": "", + "criticalTemperature": "", + "criticalPressure": "", + "autoIgnitionTemperatureInAirAt1atm": "", + "solubilityInTrichloroethylene": "", + "penetrationAt25C100g5sec": "", + "softeningPoint": "", + "ductilityAt25C": "", + "olefin": "", + "color": "", + "odor": "", + "grossCalorificValueAt15C": "", + "netCalorificValueAt15C": "", + "airRequiredForCombustion": "", + "copperCorrosionAt38CFor1Hour": "" + }, + "chemicalSpecs": { + "microCarbonResidue": "9.68", + "aromaticsTotalBTEX": "0.23", + "sedimentAndWater": "188", + "liquidPhaseH2S": "", + "mercury": "", + "oxygenates": "", + "filterableSolids": "", + "phosphorousVolatile": "", + "mediumChainTriglycerides": "", + "benzene": "", + "particulates": "", + "organicChlorides": "", + "nickel": "54", + "vanadium": "132.5", + "water": "", + "molecularWeight": "", + "sulphur": "3.66", + "naphthenes": "", + "chloride": "", + "arsenic": "", + "lead": "", + "ethene": "", + "propane": "", + "isoButane": "", + "nButane": "", + "hydrocarbonsHeavier": "", + "unsaturatedHydrocarbons": "" + } + } +}