diff --git a/cmd/vc-rest/startcmd/start.go b/cmd/vc-rest/startcmd/start.go index d6c777cbc..eec95bf29 100644 --- a/cmd/vc-rest/startcmd/start.go +++ b/cmd/vc-rest/startcmd/start.go @@ -91,6 +91,7 @@ import ( "github.com/trustbloc/vcs/pkg/service/clientidscheme" clientmanagersvc "github.com/trustbloc/vcs/pkg/service/clientmanager" credentialstatustypes "github.com/trustbloc/vcs/pkg/service/credentialstatus" + "github.com/trustbloc/vcs/pkg/service/credentialstatus/cslservice" "github.com/trustbloc/vcs/pkg/service/didconfiguration" "github.com/trustbloc/vcs/pkg/service/issuecredential" "github.com/trustbloc/vcs/pkg/service/oidc4ci" @@ -580,17 +581,21 @@ func buildEchoHandler( return nil, err } + cslService := cslservice.New(&cslservice.Config{ + CSLStore: cslVCStore, + ProfileService: issuerProfileSvc, + KMSRegistry: kmsRegistry, + Crypto: vcCrypto, + DocumentLoader: documentLoader, + }) + // Create event service eventSvc, err := event.Initialize(event.Config{ TLSConfig: tlsConfig, CMD: cmd, - CSLVCStore: cslVCStore, - ProfileService: issuerProfileSvc, - KMSRegistry: kmsRegistry, - Crypto: vcCrypto, Tracer: conf.Tracer, IsTraceEnabled: conf.IsTraceEnabled, - DocumentLoader: documentLoader, + CSLService: cslService, }) if err != nil { return nil, err @@ -606,6 +611,7 @@ func buildEchoHandler( RequestTokens: conf.StartupParameters.requestTokens, DocumentLoader: documentLoader, CSLVCStore: cslVCStore, + CSLIndexStore: cslIndexStore, CSLManager: cslManager, VCStatusStore: vcStatusStore, ProfileService: issuerProfileSvc, diff --git a/component/credentialstatus/credentialstatus_service.go b/component/credentialstatus/credentialstatus_service.go index 1332ee5dd..745bf2577 100644 --- a/component/credentialstatus/credentialstatus_service.go +++ b/component/credentialstatus/credentialstatus_service.go @@ -93,6 +93,7 @@ type Config struct { RequestTokens map[string]string VDR vdrapi.Registry CSLVCStore credentialstatus.CSLVCStore + CSLIndexStore credentialstatus.CSLIndexStore CSLManager cslManager VCStatusStore vcStatusStore Crypto vcCrypto @@ -111,6 +112,7 @@ type Service struct { requestTokens map[string]string vdr vdrapi.Registry cslVCStore credentialstatus.CSLVCStore + cslIndexStore credentialstatus.CSLIndexStore cslMgr cslManager vcStatusStore vcStatusStore crypto vcCrypto @@ -131,6 +133,7 @@ func New(config *Config) (*Service, error) { requestTokens: config.RequestTokens, vdr: config.VDR, cslVCStore: config.CSLVCStore, + cslIndexStore: config.CSLIndexStore, cslMgr: config.CSLManager, vcStatusStore: config.VCStatusStore, crypto: config.Crypto, diff --git a/component/credentialstatus/credentialstatus_service_test.go b/component/credentialstatus/credentialstatus_service_test.go index fd64b5621..1d3b071dd 100644 --- a/component/credentialstatus/credentialstatus_service_test.go +++ b/component/credentialstatus/credentialstatus_service_test.go @@ -44,6 +44,7 @@ import ( "github.com/trustbloc/vcs/pkg/event/spi" profileapi "github.com/trustbloc/vcs/pkg/profile" "github.com/trustbloc/vcs/pkg/service/credentialstatus" + "github.com/trustbloc/vcs/pkg/service/credentialstatus/cslservice" "github.com/trustbloc/vcs/pkg/service/credentialstatus/eventhandler" ) @@ -387,13 +388,17 @@ func TestCredentialStatusList_UpdateVCStatus(t *testing.T) { }) require.NoError(t, err) + cslService := cslservice.New(&cslservice.Config{ + CSLStore: cslVCStore, + ProfileService: mockProfileSrv, + KMSRegistry: mockKMSRegistry, + Crypto: crypto, + DocumentLoader: loader, + }) + mockEventPublisher := &mockedEventPublisher{ eventHandler: eventhandler.New(&eventhandler.Config{ - CSLVCStore: cslVCStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, - DocumentLoader: loader, + CSLService: cslService, }), } @@ -820,13 +825,17 @@ func TestCredentialStatusList_UpdateVCStatus(t *testing.T) { require.NoError(t, err) + cslService := cslservice.New(&cslservice.Config{ + CSLStore: cslVCStore, + ProfileService: mockProfileSrv, + KMSRegistry: mockKMSRegistry, + Crypto: crypto, + DocumentLoader: loader, + }) + mockEventPublisher := &mockedEventPublisher{ eventHandler: eventhandler.New(&eventhandler.Config{ - CSLVCStore: cslVCStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, - DocumentLoader: loader, + CSLService: cslService, }), } diff --git a/component/event/bus.go b/component/event/bus.go index f0f3394ad..a118adcd1 100644 --- a/component/event/bus.go +++ b/component/event/bus.go @@ -11,20 +11,14 @@ import ( "crypto/tls" "sync" - "github.com/piprate/json-gold/ld" "github.com/spf13/cobra" "github.com/trustbloc/logutil-go/pkg/log" - "github.com/trustbloc/vc-go/verifiable" "go.opentelemetry.io/otel/trace" "github.com/trustbloc/vcs/internal/logfields" - "github.com/trustbloc/vcs/pkg/doc/vc" - vccrypto "github.com/trustbloc/vcs/pkg/doc/vc/crypto" "github.com/trustbloc/vcs/pkg/event/spi" - vcskms "github.com/trustbloc/vcs/pkg/kms" "github.com/trustbloc/vcs/pkg/lifecycle" - profileapi "github.com/trustbloc/vcs/pkg/profile" - "github.com/trustbloc/vcs/pkg/service/credentialstatus" + "github.com/trustbloc/vcs/pkg/service/credentialstatus/eventhandler" ) var logger = log.New("event-bus") @@ -33,30 +27,13 @@ const ( defaultBufferSize = 250 ) -type profileService interface { - GetProfile(profileID profileapi.ID, profileVersion profileapi.Version) (*profileapi.Issuer, error) -} - -type kmsRegistry interface { - GetKeyManager(config *vcskms.Config) (vcskms.VCSKeyManager, error) -} - -type vcCrypto interface { - SignCredential(signerData *vc.Signer, vc *verifiable.Credential, - opts ...vccrypto.SigningOpts) (*verifiable.Credential, error) -} - // Config holds the configuration for the publisher/subscriber. type Config struct { TLSConfig *tls.Config CMD *cobra.Command - CSLVCStore credentialstatus.CSLVCStore - ProfileService profileService - KMSRegistry kmsRegistry - Crypto vcCrypto Tracer trace.Tracer IsTraceEnabled bool - DocumentLoader ld.DocumentLoader + CSLService eventhandler.CSLService } // Bus implements a publisher/subscriber using Go channels. This implementation diff --git a/component/event/event.go b/component/event/event.go index cf73bee0a..ebddb3db9 100644 --- a/component/event/event.go +++ b/component/event/event.go @@ -64,11 +64,7 @@ func Initialize(cfg Config) (*Bus, error) { } service := credentialstatuseventhandler.New(&credentialstatuseventhandler.Config{ - CSLVCStore: cfg.CSLVCStore, - ProfileService: cfg.ProfileService, - KMSRegistry: cfg.KMSRegistry, - Crypto: cfg.Crypto, - DocumentLoader: cfg.DocumentLoader, + CSLService: cfg.CSLService, }) var credentialStatusEventHandler eventHandlerWithContext = service.HandleEvent diff --git a/pkg/service/credentialstatus/cslservice/cslservice.go b/pkg/service/credentialstatus/cslservice/cslservice.go new file mode 100644 index 000000000..f51318582 --- /dev/null +++ b/pkg/service/credentialstatus/cslservice/cslservice.go @@ -0,0 +1,201 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package cslservice + +import ( + "context" + "fmt" + + "github.com/piprate/json-gold/ld" + "github.com/trustbloc/vc-go/dataintegrity/models" + "github.com/trustbloc/vc-go/verifiable" + + "github.com/trustbloc/vcs/pkg/doc/vc" + vccrypto "github.com/trustbloc/vcs/pkg/doc/vc/crypto" + vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" + vcskms "github.com/trustbloc/vcs/pkg/kms" + profileapi "github.com/trustbloc/vcs/pkg/profile" + "github.com/trustbloc/vcs/pkg/service/credentialstatus" +) + +//go:generate mockgen -destination cslservice_mocks_test.go -self_package mocks -package cslservice -source=cslservice.go -mock_names profileService=MockProfileService,kmsRegistry=MockKMSRegistry + +const ( + defaultRepresentation = "jws" + + jsonKeyProofValue = "proofValue" + jsonKeyProofPurpose = "proofPurpose" + jsonKeyVerificationMethod = "verificationMethod" + jsonKeySignatureOfType = "type" +) + +type profileService interface { + GetProfile(profileID profileapi.ID, profileVersion profileapi.Version) (*profileapi.Issuer, error) +} + +type kmsRegistry interface { + GetKeyManager(config *vcskms.Config) (vcskms.VCSKeyManager, error) +} + +type vcCrypto interface { + SignCredential(signerData *vc.Signer, vc *verifiable.Credential, + opts ...vccrypto.SigningOpts) (*verifiable.Credential, error) +} + +type Config struct { + CSLStore credentialstatus.CSLVCStore + ProfileService profileService + KMSRegistry kmsRegistry + Crypto vcCrypto + DocumentLoader ld.DocumentLoader +} + +type Service struct { + cslStore credentialstatus.CSLVCStore + profileService profileService + kmsRegistry kmsRegistry + crypto vcCrypto + documentLoader ld.DocumentLoader +} + +func New(cfg *Config) *Service { + return &Service{ + cslStore: cfg.CSLStore, + profileService: cfg.ProfileService, + kmsRegistry: cfg.KMSRegistry, + crypto: cfg.Crypto, + documentLoader: cfg.DocumentLoader, + } +} + +func (s *Service) SignCSL(profileID, profileVersion string, csl *verifiable.Credential) ([]byte, error) { + issuerProfile, err := s.profileService.GetProfile(profileID, profileVersion) + if err != nil { + return nil, fmt.Errorf("failed to get profile: %w", err) + } + + keyManager, err := s.kmsRegistry.GetKeyManager(issuerProfile.KMSConfig) + if err != nil { + return nil, fmt.Errorf("failed to get KMS: %w", err) + } + + signer := &vc.Signer{ + Format: issuerProfile.VCConfig.Format, + DID: issuerProfile.SigningDID.DID, + Creator: issuerProfile.SigningDID.Creator, + KMSKeyID: issuerProfile.SigningDID.KMSKeyID, + SignatureType: issuerProfile.VCConfig.SigningAlgorithm, + KeyType: issuerProfile.VCConfig.KeyType, + KMS: keyManager, + SignatureRepresentation: issuerProfile.VCConfig.SignatureRepresentation, + VCStatusListType: issuerProfile.VCConfig.Status.Type, + SDJWT: vc.SDJWT{Enable: false}, + DataIntegrityProof: issuerProfile.VCConfig.DataIntegrityProof, + } + + signOpts, err := prepareSigningOpts(signer, csl.Proofs()) + if err != nil { + return nil, fmt.Errorf("prepareSigningOpts failed: %w", err) + } + + signedCredential, err := s.crypto.SignCredential(signer, csl, signOpts...) + if err != nil { + return nil, fmt.Errorf("sign CSL failed: %w", err) + } + + return signedCredential.MarshalJSON() +} + +func (s *Service) GetCSLVCWrapper(ctx context.Context, cslURL string) (*credentialstatus.CSLVCWrapper, error) { + vcWrapper, err := s.cslStore.Get(ctx, cslURL) + if err != nil { + return nil, fmt.Errorf("failed to get CSL from store: %w", err) + } + + cslVC, err := verifiable.ParseCredential(vcWrapper.VCByte, + verifiable.WithDisabledProofCheck(), + verifiable.WithJSONLDDocumentLoader(s.documentLoader)) + if err != nil { + return nil, fmt.Errorf("failed to parse CSL: %w", err) + } + + vcWrapper.VC = cslVC + + return vcWrapper, nil +} + +func (s *Service) UpsertCSLVCWrapper(ctx context.Context, cslURL string, wrapper *credentialstatus.CSLVCWrapper) error { + if err := s.cslStore.Upsert(ctx, cslURL, wrapper); err != nil { + return fmt.Errorf("failed to upsert CSL: %w", err) + } + + return nil +} + +// prepareSigningOpts prepares signing opts from recently issued proof of given credential. +func prepareSigningOpts(profile *vc.Signer, proofs []verifiable.Proof) ([]vccrypto.SigningOpts, error) { + var signingOpts []vccrypto.SigningOpts + + if len(proofs) == 0 { + return signingOpts, nil + } + + // pick latest proof if there are multiple + proof := proofs[len(proofs)-1] + + representation := defaultRepresentation + if _, ok := proof[jsonKeyProofValue]; ok { + representation = jsonKeyProofValue + } + + signingOpts = append(signingOpts, vccrypto.WithSigningRepresentation(representation)) + + purpose, err := getStringValue(jsonKeyProofPurpose, proof) + if err != nil { + return nil, err + } + + signingOpts = append(signingOpts, vccrypto.WithPurpose(purpose)) + + vm, err := getStringValue(jsonKeyVerificationMethod, proof) + if err != nil { + return nil, err + } + + // add verification method option only when it is not matching profile creator + if vm != profile.Creator { + signingOpts = append(signingOpts, vccrypto.WithVerificationMethod(vm)) + } + + signTypeName, err := getStringValue(jsonKeySignatureOfType, proof) + if err != nil { + return nil, err + } + + if signTypeName != "" && signTypeName != models.DataIntegrityProof { + signType, err := vcsverifiable.GetSignatureTypeByName(signTypeName) + if err != nil { + return nil, err + } + + signingOpts = append(signingOpts, vccrypto.WithSignatureType(signType)) + } + + return signingOpts, 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/service/credentialstatus/cslservice/cslservice_test.go b/pkg/service/credentialstatus/cslservice/cslservice_test.go new file mode 100644 index 000000000..09fa228fb --- /dev/null +++ b/pkg/service/credentialstatus/cslservice/cslservice_test.go @@ -0,0 +1,582 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package cslservice + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/json" + "errors" + "net/url" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/multiformats/go-multibase" + "github.com/piprate/json-gold/ld" + "github.com/stretchr/testify/require" + "github.com/trustbloc/did-go/doc/did" + vdrmock "github.com/trustbloc/did-go/vdr/mock" + "github.com/trustbloc/kms-go/spi/kms" + "github.com/trustbloc/vc-go/dataintegrity/suite/eddsa2022" + "github.com/trustbloc/vc-go/verifiable" + + "github.com/trustbloc/vcs/internal/mock/vcskms" + "github.com/trustbloc/vcs/pkg/doc/vc" + "github.com/trustbloc/vcs/pkg/doc/vc/bitstring" + vccrypto "github.com/trustbloc/vcs/pkg/doc/vc/crypto" + vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" + "github.com/trustbloc/vcs/pkg/internal/testutil" + profileapi "github.com/trustbloc/vcs/pkg/profile" + "github.com/trustbloc/vcs/pkg/service/credentialstatus" +) + +func TestService_signCSL(t *testing.T) { + loader := testutil.DocumentLoader(t) + ctx := context.Background() + mockKMSRegistry := NewMockKMSRegistry(gomock.NewController(t)) + mockKMSRegistry.EXPECT().GetKeyManager(gomock.Any()).AnyTimes().Return(&vcskms.MockKMS{}, nil) + crypto := vccrypto.New( + &vdrmock.VDRegistry{ResolveValue: createDIDDoc("did:test:abc")}, loader) + + t.Run("OK", func(t *testing.T) { + profile := getTestProfile(vc.StatusList2021VCStatus) + + mockProfileSrv := NewMockProfileService(gomock.NewController(t)) + mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) + + cslStore := newMockCSLVCStore() + + var cslWrapper *credentialstatus.CSLVCWrapper + err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) + require.NoError(t, err) + cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) + + err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) + require.NoError(t, err) + + s := New(&Config{ + DocumentLoader: loader, + CSLStore: cslStore, + ProfileService: mockProfileSrv, + KMSRegistry: mockKMSRegistry, + Crypto: crypto, + }) + + signedCSL, err := s.SignCSL(profileID, profileVersion, cslWrapper.VC) + require.NoError(t, err) + require.NotEmpty(t, signedCSL) + cslWrapper.VC = getVerifiedCSL(t, signedCSL, loader, statusBytePositionIndex, false) + require.NotEmpty(t, cslWrapper.VC.Proofs) + }) + + t.Run("BitstringStatusList -> OK", func(t *testing.T) { + profile := getTestProfile(vc.BitstringStatusList) + profile.VCConfig.DataIntegrityProof = vc.DataIntegrityProofConfig{ + Enable: true, + SuiteType: eddsa2022.SuiteType, + } + + mockProfileSrv := NewMockProfileService(gomock.NewController(t)) + mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) + + cslStore := newMockCSLVCStore() + + var cslWrapper *credentialstatus.CSLVCWrapper + err := json.Unmarshal([]byte(cslWrapperBitstringBytes), &cslWrapper) + require.NoError(t, err) + cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) + + err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) + require.NoError(t, err) + + s := New(&Config{ + DocumentLoader: loader, + CSLStore: cslStore, + ProfileService: mockProfileSrv, + KMSRegistry: mockKMSRegistry, + Crypto: crypto, + }) + + signedCSL, err := s.SignCSL(profileID, profileVersion, cslWrapper.VC) + require.NoError(t, err) + require.NotEmpty(t, signedCSL) + cslWrapper.VC = getVerifiedCSL(t, signedCSL, loader, statusBytePositionIndex, false) + require.NotEmpty(t, cslWrapper.VC.Proofs) + }) + + t.Run("Error failed to get profile", func(t *testing.T) { + mockProfileSrvErr := NewMockProfileService(gomock.NewController(t)) + mockProfileSrvErr.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(nil, errors.New("some error")) + s := New(&Config{ + ProfileService: mockProfileSrvErr, + }) + + signedCSL, err := s.SignCSL(profileID, profileVersion, nil) + require.Empty(t, signedCSL) + require.Error(t, err) + require.ErrorContains(t, err, "failed to get profile") + }) + + t.Run("Error failed to get KMS", func(t *testing.T) { + profile := getTestProfile(vc.StatusList2021VCStatus) + + mockProfileSrv := NewMockProfileService(gomock.NewController(t)) + mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) + + mockKMSRegistryErr := NewMockKMSRegistry(gomock.NewController(t)) + mockKMSRegistryErr.EXPECT().GetKeyManager(gomock.Any()).AnyTimes().Return(nil, errors.New("some error")) + s := New(&Config{ + ProfileService: mockProfileSrv, + KMSRegistry: mockKMSRegistryErr, + }) + + signedCSL, err := s.SignCSL(profileID, profileVersion, nil) + require.Empty(t, signedCSL) + require.Error(t, err) + require.ErrorContains(t, err, "failed to get KMS") + }) + + t.Run("Error prepareSigningOpts failed", func(t *testing.T) { + profile := getTestProfile(vc.StatusList2021VCStatus) + + mockProfileSrv := NewMockProfileService(gomock.NewController(t)) + mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) + + var cslWrapper *credentialstatus.CSLVCWrapper + err := json.Unmarshal([]byte(cslWrapperBytesInvalidProof), &cslWrapper) + require.NoError(t, err) + + cslWrapper.VC, err = verifiable.ParseCredential(cslWrapper.VCByte, + verifiable.WithDisabledProofCheck(), + verifiable.WithCredDisableValidation(), + verifiable.WithJSONLDDocumentLoader(loader)) + require.NoError(t, err) + + s := New(&Config{ + ProfileService: mockProfileSrv, + KMSRegistry: mockKMSRegistry, + }) + + signedCSL, err := s.SignCSL(profileID, profileVersion, cslWrapper.VC) + require.Empty(t, signedCSL) + require.Error(t, err) + require.ErrorContains(t, err, "prepareSigningOpts failed") + }) + + t.Run("Error sign CSL failed", func(t *testing.T) { + profile := getTestProfile(vc.StatusList2021VCStatus) + + mockProfileSrv := NewMockProfileService(gomock.NewController(t)) + mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) + + cryptoErr := vccrypto.New( + &vdrmock.VDRegistry{ResolveErr: errors.New("some error")}, loader) + var cslWrapper *credentialstatus.CSLVCWrapper + err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) + require.NoError(t, err) + cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) + + s := New(&Config{ + DocumentLoader: loader, + ProfileService: mockProfileSrv, + KMSRegistry: mockKMSRegistry, + Crypto: cryptoErr, + }) + + signedCSL, err := s.SignCSL(profileID, profileVersion, cslWrapper.VC) + require.Empty(t, signedCSL) + require.Error(t, err) + require.ErrorContains(t, err, "sign CSL failed") + }) +} + +func TestPrepareSigningOpts(t *testing.T) { + t.Parallel() + + t.Run("prepare signing opts", func(t *testing.T) { + profile := &vc.Signer{ + Creator: "did:creator#key-1", + } + + tests := []struct { + name string + proof string + result int + count int + err string + }{ + { + name: "prepare proofvalue signing opts", + proof: `{ + "created": "2020-04-17T04:17:48Z", + "proofPurpose": "assertionMethod", + "proofValue": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8AUdCJDRptPkBuqAQ", + "type": "Ed25519Signature2018", + "verificationMethod": "did:trustbloc:testnet.trustbloc.local#key-1" + }`, + }, + { + name: "prepare jws signing opts", + proof: `{ + "created": "2020-04-17T04:17:48Z", + "proofPurpose": "assertionMethod", + "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", + "type": "Ed25519Signature2018", + "verificationMethod": "did:creator#key-1" + }`, + count: 3, + }, + { + name: "prepare signing opts from proof with 3 required properties", + proof: `{ + "created": "2020-04-17T04:17:48Z", + "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", + "type": "Ed25519Signature2018", + "verificationMethod": "did:example:EiABBmUZ7JjpKSTNGq9Q==#key-1" + }`, + }, + { + name: "prepare signing opts from proof with 2 required properties", + proof: `{ + "created": "2020-04-17T04:17:48Z", + "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", + "verificationMethod": "did:example:EiABBmUZ7JjpKSTNGq9Q==#key-1" + }`, + }, + { + name: "prepare signing opts from proof with 1 required property", + proof: `{ + "created": "2020-04-17T04:17:48Z", + "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ" + }`, + }, + { + name: "prepare jws signing opts - invalid purpose", + proof: `{ + "created": "2020-04-17T04:17:48Z", + "proofPurpose": {}, + "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", + "type": "Ed25519Signature2018", + "verificationMethod": "did:example:EiABBmUZ7JjpKSTNGq9Q==#key-1" + }`, + err: "invalid 'proofPurpose' type", + }, + { + name: "prepare jws signing opts - invalid signature type", + proof: `{ + "created": "2020-04-17T04:17:48Z", + "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", + "type": {}, + "verificationMethod": "did:example:EiABBmUZ7JjpKSTNGq9Q==#key-1" + }`, + err: "invalid 'type' type", + }, + { + name: "prepare jws signing opts - invalid signature type", + proof: `{ + "created": "2020-04-17T04:17:48Z", + "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", + "type": {}, + "verificationMethod": {} + }`, + err: "invalid 'verificationMethod' type", + }, + } + + t.Parallel() + + for _, test := range tests { + tc := test + t.Run(tc.name, func(t *testing.T) { + var proof map[string]interface{} + err := json.Unmarshal([]byte(tc.proof), &proof) + require.NoError(t, err) + + opts, err := prepareSigningOpts(profile, []verifiable.Proof{proof}) + + if tc.err != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.err) + return + } + + if tc.count > 0 { + require.Len(t, opts, tc.count) + } + + require.NoError(t, err) + require.NotEmpty(t, opts) + }) + } + }) +} + +func TestService_UpsertAndGetCSLWrapper(t *testing.T) { + loader := testutil.DocumentLoader(t) + + var cslWrapper *credentialstatus.CSLVCWrapper + require.NoError(t, json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper)) + + cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) + + cslStore := newMockCSLVCStore() + + s := New(&Config{ + CSLStore: cslStore, + DocumentLoader: loader, + }) + + require.NoError(t, s.UpsertCSLVCWrapper(context.Background(), cslWrapper.VC.Contents().ID, cslWrapper)) + + loadedCSLWrapper, err := s.GetCSLVCWrapper(context.Background(), cslWrapper.VC.Contents().ID) + require.NoError(t, err) + require.Equal(t, cslWrapper, loadedCSLWrapper) +} + +type mockCSLVCStore struct { + createErr error + getCSLErr error + findErr error + s map[string]*credentialstatus.CSLVCWrapper +} + +func newMockCSLVCStore() *mockCSLVCStore { + s := &mockCSLVCStore{ + s: map[string]*credentialstatus.CSLVCWrapper{}, + } + + return s +} + +func (m *mockCSLVCStore) GetCSLURL(issuerURL, issuerID string, listID credentialstatus.ListID) (string, error) { + if m.getCSLErr != nil { + return "", m.getCSLErr + } + + return url.JoinPath(issuerURL, "issuer/profiles", issuerID, "credentials/status", string(listID)) +} + +func (m *mockCSLVCStore) Upsert(_ context.Context, cslURL string, cslWrapper *credentialstatus.CSLVCWrapper) error { + if m.createErr != nil { + return m.createErr + } + + m.s[cslURL] = cslWrapper + + return nil +} + +func (m *mockCSLVCStore) Get(_ context.Context, cslURL string) (*credentialstatus.CSLVCWrapper, error) { + if m.findErr != nil { + return nil, m.findErr + } + + w, ok := m.s[cslURL] + if !ok { + return nil, credentialstatus.ErrDataNotFound + } + + return w, nil +} + +func createDIDDoc(didID string) *did.Doc { + const ( + didContext = "https://w3id.org/did/v1" + keyType = "Ed25519VerificationKey2018" + ) + + pubKey, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + panic(err) + } + + creator := didID + "#key1" + + service := did.Service{ + ID: "did:example:123456789abcdefghi#did-communication", + RecipientKeys: []string{creator}, + Priority: 0, + } + + signingKey := did.VerificationMethod{ + ID: creator, + Type: keyType, + Controller: didID, + Value: pubKey, + } + + createdTime := time.Now() + + return &did.Doc{ + Context: []string{didContext}, + ID: didID, + VerificationMethod: []did.VerificationMethod{signingKey}, + Service: []did.Service{service}, + Created: &createdTime, + AssertionMethod: []did.Verification{{VerificationMethod: signingKey}}, + Authentication: []did.Verification{{VerificationMethod: signingKey}}, + CapabilityInvocation: []did.Verification{{VerificationMethod: signingKey}}, + CapabilityDelegation: []did.Verification{{VerificationMethod: signingKey}}, + } +} + +func getTestProfile(statusType vc.StatusType) *profileapi.Issuer { + return &profileapi.Issuer{ + ID: profileID, + Name: "testprofile", + GroupID: "externalID", + VCConfig: &profileapi.VCConfig{ + Format: vcsverifiable.Ldp, + SigningAlgorithm: "Ed25519Signature2018", + KeyType: kms.ED25519Type, + Status: profileapi.StatusConfig{ + Type: statusType, + }, + }, + SigningDID: &profileapi.SigningDID{ + DID: "did:test:abc", + Creator: "did:test:abc#key1", + }, + } +} + +//nolint:unparam +func getVerifiedCSL( + t *testing.T, cslBytes []byte, dl ld.DocumentLoader, index int, expectedStatus bool) *verifiable.Credential { + t.Helper() + csl, err := verifiable.ParseCredential(cslBytes, + verifiable.WithDisabledProofCheck(), + verifiable.WithJSONLDDocumentLoader(dl)) + require.NoError(t, err) + + credSubject := csl.Contents().Subject + require.NotEmpty(t, credSubject[0].CustomFields["encodedList"].(string)) + + var bitString *bitstring.BitString + + statusType, ok := credSubject[0].CustomFields["type"].(string) + require.True(t, ok) + + if statusType == "BitstringStatusList" { + bitString, err = bitstring.DecodeBits(credSubject[0].CustomFields["encodedList"].(string), + bitstring.WithMultibaseEncoding(multibase.Base64url)) + } else { + bitString, err = bitstring.DecodeBits(credSubject[0].CustomFields["encodedList"].(string)) + } + + require.NoError(t, err) + bitSet, err := bitString.Get(index) + require.NoError(t, err) + require.Equal(t, expectedStatus, bitSet) + + return csl +} + +const ( + profileID = "testProfileID" + profileVersion = "v1.0" + statusBytePositionIndex = 1 + listUUID = "d715ce6b-0df5-4fe8-ab19-be9bc6dada9c" + cslURL = "https://localhost:8080/issuer/profiles/externalID/credentials/status/" + listUUID + + cslWrapperBytes = `{ + "vc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "credentialSubject": { + "encodedList": "H4sIAAAAAAAA_-zAgQAAAACAoP2pF6kAAAAAAAAAAAAAAAAAAACgOgAA__-N53xXgD4AAA", + "id": "` + cslURL + `#list", + "statusPurpose": "revocation", + "type": "StatusList2021" + }, + "id": "` + cslURL + `", + "issuanceDate": "2023-03-22T11:34:05.091926539Z", + "issuer": "did:test:abc", + "type": [ + "VerifiableCredential", + "StatusList2021Credential" + ] + } +}` + cslWrapperBytesInvalidProof = `{ + "vc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "credentialSubject": { + "encodedList": "H4sIAAAAAAAA_-zAgQAAAACAoP2pF6kAAAAAAAAAAAAAAAAAAACgOgAA__-N53xXgD4AAA", + "id": "` + cslURL + `#list", + "statusPurpose": "revocation", + "type": "StatusList2021" + }, + "id": "` + cslURL + `", + "issuanceDate": "2023-03-22T11:34:05.091926539Z", + "issuer": "did:test:abc", + "type": [ + "VerifiableCredential", + "StatusList2021Credential" + ], + "proof": { + "proofPurpose": 123 + } + } +}` + //nolint:lll + cslWrapperBitstringBytes = `{ + "vc": { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "credentialSubject": { + "encodedList": "uH4sIAAAAAAAA_-zAgQAAAACAoP2pF6kAAAAAAAAAAAAAAAAAAACgOgAA__-N53xXgD4AAA", + "id": "https://localhost:8080/issuer/profiles/externalID/credentials/status/05bd0c2e-5a06-4393-849c-42330f02a3f7#list", + "statusPurpose": "revocation", + "type": "BitstringStatusList" + }, + "id": "https://localhost:8080/issuer/profiles/externalID/credentials/status/05bd0c2e-5a06-4393-849c-42330f02a3f7", + "issuer": "did:test:abc", + "proof": { + "created": "2024-11-26T13:46:18-05:00", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z", + "type": "DataIntegrityProof", + "verificationMethod": "did:test:abc#key1" + }, + "type": [ + "VerifiableCredential", + "BitstringStatusListCredential" + ], + "validFrom": "2024-11-26T18:46:18.403867Z" + } +}` + cslBytes = `{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "credentialSubject": { + "encodedList": "H4sIAAAAAAAA_-zAgQAAAACAoP2pF6kAAAAAAAAAAAAAAAAAAACgOgAA__-N53xXgD4AAA", + "id": "` + cslURL + `#list", + "statusPurpose": "revocation", + "type": "StatusList2021" + }, + "id": "` + cslURL + `", + "issuanceDate": "2023-03-22T11:34:05.091926539Z", + "issuer": "did:test:abc", + "type": [ + "VerifiableCredential", + "StatusList2021Credential" + ] +}` +) diff --git a/pkg/service/credentialstatus/eventhandler/eventhandler_service.go b/pkg/service/credentialstatus/eventhandler/eventhandler_service.go index 01fc1bcbe..d900047fd 100644 --- a/pkg/service/credentialstatus/eventhandler/eventhandler_service.go +++ b/pkg/service/credentialstatus/eventhandler/eventhandler_service.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -//go:generate mockgen -destination service_mocks_test.go -self_package mocks -package eventhandler -source=eventhandler_service.go -mock_names profileService=MockProfileService,kmsRegistry=MockKMSRegistry +//go:generate mockgen -destination service_mocks_test.go -self_package mocks -package eventhandler -source=eventhandler_service.go -mock_names CSLService=MockCSLService package eventhandler @@ -13,70 +13,37 @@ import ( "encoding/json" "fmt" - "github.com/piprate/json-gold/ld" "github.com/trustbloc/logutil-go/pkg/log" - "github.com/trustbloc/vc-go/dataintegrity/models" "github.com/trustbloc/vc-go/verifiable" - vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" "github.com/trustbloc/vcs/internal/logfields" "github.com/trustbloc/vcs/pkg/doc/vc" - vccrypto "github.com/trustbloc/vcs/pkg/doc/vc/crypto" "github.com/trustbloc/vcs/pkg/doc/vc/statustype" "github.com/trustbloc/vcs/pkg/event/spi" - vcskms "github.com/trustbloc/vcs/pkg/kms" - profileapi "github.com/trustbloc/vcs/pkg/profile" "github.com/trustbloc/vcs/pkg/service/credentialstatus" ) -const ( - defaultRepresentation = "jws" - - jsonKeyProofValue = "proofValue" - jsonKeyProofPurpose = "proofPurpose" - jsonKeyVerificationMethod = "verificationMethod" - jsonKeySignatureOfType = "type" - jsonStatusListType = "type" -) - var logger = log.New("credentialstatus-eventhandler") -type profileService interface { - GetProfile(profileID profileapi.ID, profileVersion profileapi.Version) (*profileapi.Issuer, error) -} - -type kmsRegistry interface { - GetKeyManager(config *vcskms.Config) (vcskms.VCSKeyManager, error) -} +const jsonStatusListType = "type" -type vcCrypto interface { - SignCredential(signerData *vc.Signer, vc *verifiable.Credential, - opts ...vccrypto.SigningOpts) (*verifiable.Credential, error) +type CSLService interface { + SignCSL(profileID, profileVersion string, csl *verifiable.Credential) ([]byte, error) + GetCSLVCWrapper(ctx context.Context, cslURL string) (*credentialstatus.CSLVCWrapper, error) + UpsertCSLVCWrapper(ctx context.Context, cslURL string, wrapper *credentialstatus.CSLVCWrapper) error } type Config struct { - CSLVCStore credentialstatus.CSLVCStore - ProfileService profileService - KMSRegistry kmsRegistry - Crypto vcCrypto - DocumentLoader ld.DocumentLoader + CSLService CSLService } type Service struct { - cslStore credentialstatus.CSLVCStore - profileService profileService - kmsRegistry kmsRegistry - crypto vcCrypto - documentLoader ld.DocumentLoader + cslService CSLService } func New(conf *Config) *Service { return &Service{ - cslStore: conf.CSLVCStore, - profileService: conf.ProfileService, - kmsRegistry: conf.KMSRegistry, - crypto: conf.Crypto, - documentLoader: conf.DocumentLoader, + cslService: conf.CSLService, } } @@ -90,7 +57,12 @@ func (s *Service) HandleEvent(ctx context.Context, event *spi.Event) error { //n payload := credentialstatus.UpdateCredentialStatusEventPayload{} - jsonData, err := json.Marshal(event.Data.(map[string]interface{})) + doc, ok := event.Data.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid event data") + } + + jsonData, err := json.Marshal(doc) if err != nil { return err } @@ -104,7 +76,7 @@ func (s *Service) HandleEvent(ctx context.Context, event *spi.Event) error { //n func (s *Service) handleEventPayload( ctx context.Context, payload credentialstatus.UpdateCredentialStatusEventPayload) error { - clsWrapper, err := s.getCSLVCWrapper(ctx, payload.CSLURL) + clsWrapper, err := s.cslService.GetCSLVCWrapper(ctx, payload.CSLURL) if err != nil { return fmt.Errorf("get CSL VC wrapper failed: %w", err) } @@ -126,7 +98,7 @@ func (s *Service) handleEventPayload( return fmt.Errorf("failed to update status: %w", err) } - signedCredentialBytes, err := s.signCSL(payload.ProfileID, payload.ProfileVersion, clsWrapper.VC) + signedCredentialBytes, err := s.cslService.SignCSL(payload.ProfileID, payload.ProfileVersion, clsWrapper.VC) if err != nil { return fmt.Errorf("failed to sign CSL: %w", err) } @@ -135,121 +107,13 @@ func (s *Service) handleEventPayload( VCByte: signedCredentialBytes, } - if err = s.cslStore.Upsert(ctx, payload.CSLURL, vcWrapper); err != nil { - return fmt.Errorf("cslStore.Upsert failed: %w", err) + if err = s.cslService.UpsertCSLVCWrapper(ctx, payload.CSLURL, vcWrapper); err != nil { + return fmt.Errorf("save CSL failed: %w", err) } return nil } -func (s *Service) signCSL(profileID, profileVersion string, csl *verifiable.Credential) ([]byte, error) { - issuerProfile, err := s.profileService.GetProfile(profileID, profileVersion) - if err != nil { - return nil, fmt.Errorf("failed to get profile: %w", err) - } - - keyManager, err := s.kmsRegistry.GetKeyManager(issuerProfile.KMSConfig) - if err != nil { - return nil, fmt.Errorf("failed to get KMS: %w", err) - } - - signer := &vc.Signer{ - Format: issuerProfile.VCConfig.Format, - DID: issuerProfile.SigningDID.DID, - Creator: issuerProfile.SigningDID.Creator, - KMSKeyID: issuerProfile.SigningDID.KMSKeyID, - SignatureType: issuerProfile.VCConfig.SigningAlgorithm, - KeyType: issuerProfile.VCConfig.KeyType, - KMS: keyManager, - SignatureRepresentation: issuerProfile.VCConfig.SignatureRepresentation, - VCStatusListType: issuerProfile.VCConfig.Status.Type, - SDJWT: vc.SDJWT{Enable: false}, - DataIntegrityProof: issuerProfile.VCConfig.DataIntegrityProof, - } - - signOpts, err := prepareSigningOpts(signer, csl.Proofs()) - if err != nil { - return nil, fmt.Errorf("prepareSigningOpts failed: %w", err) - } - - signedCredential, err := s.crypto.SignCredential(signer, csl, signOpts...) - if err != nil { - return nil, fmt.Errorf("sign CSL failed: %w", err) - } - - return signedCredential.MarshalJSON() -} - -func (s *Service) getCSLVCWrapper(ctx context.Context, cslURL string) (*credentialstatus.CSLVCWrapper, error) { - vcWrapper, err := s.cslStore.Get(ctx, cslURL) - if err != nil { - return nil, fmt.Errorf("failed to get CSL from store: %w", err) - } - - cslVC, err := verifiable.ParseCredential(vcWrapper.VCByte, - verifiable.WithDisabledProofCheck(), - verifiable.WithJSONLDDocumentLoader(s.documentLoader)) - if err != nil { - return nil, fmt.Errorf("failed to parse CSL: %w", err) - } - - vcWrapper.VC = cslVC - - return vcWrapper, nil -} - -// prepareSigningOpts prepares signing opts from recently issued proof of given credential. -func prepareSigningOpts(profile *vc.Signer, proofs []verifiable.Proof) ([]vccrypto.SigningOpts, error) { - var signingOpts []vccrypto.SigningOpts - - if len(proofs) == 0 { - return signingOpts, nil - } - - // pick latest proof if there are multiple - proof := proofs[len(proofs)-1] - - representation := defaultRepresentation - if _, ok := proof[jsonKeyProofValue]; ok { - representation = jsonKeyProofValue - } - - signingOpts = append(signingOpts, vccrypto.WithSigningRepresentation(representation)) - - purpose, err := getStringValue(jsonKeyProofPurpose, proof) - if err != nil { - return nil, err - } - - signingOpts = append(signingOpts, vccrypto.WithPurpose(purpose)) - - vm, err := getStringValue(jsonKeyVerificationMethod, proof) - if err != nil { - return nil, err - } - - // add verification method option only when it is not matching profile creator - if vm != profile.Creator { - signingOpts = append(signingOpts, vccrypto.WithVerificationMethod(vm)) - } - - signTypeName, err := getStringValue(jsonKeySignatureOfType, proof) - if err != nil { - return nil, err - } - - if signTypeName != "" && signTypeName != models.DataIntegrityProof { - signType, err := vcsverifiable.GetSignatureTypeByName(signTypeName) - if err != nil { - return nil, err - } - - signingOpts = append(signingOpts, vccrypto.WithSignatureType(signType)) - } - - return signingOpts, nil -} - func getStringValue(key string, vMap map[string]interface{}) (string, error) { if val, ok := vMap[key]; ok { if s, ok := val.(string); ok { diff --git a/pkg/service/credentialstatus/eventhandler/eventhandler_service_test.go b/pkg/service/credentialstatus/eventhandler/eventhandler_service_test.go index a31159938..c0fb3354e 100644 --- a/pkg/service/credentialstatus/eventhandler/eventhandler_service_test.go +++ b/pkg/service/credentialstatus/eventhandler/eventhandler_service_test.go @@ -8,33 +8,19 @@ package eventhandler import ( "context" - "crypto/ed25519" - "crypto/rand" "encoding/json" "errors" - "net/url" "testing" - "time" "github.com/golang/mock/gomock" + "github.com/multiformats/go-multibase" "github.com/piprate/json-gold/ld" "github.com/stretchr/testify/require" - "github.com/trustbloc/kms-go/spi/kms" - - "github.com/multiformats/go-multibase" - "github.com/trustbloc/did-go/doc/did" - vdrmock "github.com/trustbloc/did-go/vdr/mock" - "github.com/trustbloc/vc-go/dataintegrity/suite/eddsa2022" "github.com/trustbloc/vc-go/verifiable" - "github.com/trustbloc/vcs/internal/mock/vcskms" - "github.com/trustbloc/vcs/pkg/doc/vc" "github.com/trustbloc/vcs/pkg/doc/vc/bitstring" - vccrypto "github.com/trustbloc/vcs/pkg/doc/vc/crypto" - vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" "github.com/trustbloc/vcs/pkg/event/spi" "github.com/trustbloc/vcs/pkg/internal/testutil" - profileapi "github.com/trustbloc/vcs/pkg/profile" "github.com/trustbloc/vcs/pkg/service/credentialstatus" ) @@ -86,113 +72,76 @@ const ( ] } }` - cslWrapperBytesInvalidProof = `{ - "vc": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/2021/v1" - ], - "credentialSubject": { - "encodedList": "H4sIAAAAAAAA_-zAgQAAAACAoP2pF6kAAAAAAAAAAAAAAAAAAACgOgAA__-N53xXgD4AAA", - "id": "` + cslURL + `#list", - "statusPurpose": "revocation", - "type": "StatusList2021" - }, - "id": "` + cslURL + `", - "issuanceDate": "2023-03-22T11:34:05.091926539Z", - "issuer": "did:test:abc", - "type": [ - "VerifiableCredential", - "StatusList2021Credential" - ], - "proof": { - "proofPurpose": 123 - } - } -}` - //nolint:lll - cslWrapperBitstringBytes = `{ - "vc": { - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://w3id.org/security/suites/ed25519-2020/v1" - ], - "credentialSubject": { - "encodedList": "uH4sIAAAAAAAA_-zAgQAAAACAoP2pF6kAAAAAAAAAAAAAAAAAAACgOgAA__-N53xXgD4AAA", - "id": "https://localhost:8080/issuer/profiles/externalID/credentials/status/05bd0c2e-5a06-4393-849c-42330f02a3f7#list", - "statusPurpose": "revocation", - "type": "BitstringStatusList" - }, - "id": "https://localhost:8080/issuer/profiles/externalID/credentials/status/05bd0c2e-5a06-4393-849c-42330f02a3f7", - "issuer": "did:test:abc", - "proof": { - "created": "2024-11-26T13:46:18-05:00", - "cryptosuite": "eddsa-rdfc-2022", - "proofPurpose": "assertionMethod", - "proofValue": "z", - "type": "DataIntegrityProof", - "verificationMethod": "did:test:abc#key1" - }, - "type": [ - "VerifiableCredential", - "BitstringStatusListCredential" - ], - "validFrom": "2024-11-26T18:46:18.403867Z" - } + //nolint + signedCSL = `{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "credentialSubject": { + "encodedList": "H4sIAAAAAAAA_-zAgQAAAACAoP2pF6kAAAAAAAAAAAAAAAAAAACgOgAA__-N53xXgD4AAA", + "id": "https://localhost:8080/issuer/profiles/externalID/credentials/status/d715ce6b-0df5-4fe8-ab19-be9bc6dada9c#list", + "statusPurpose": "revocation", + "type": "StatusList2021" + }, + "id": "https://localhost:8080/issuer/profiles/externalID/credentials/status/d715ce6b-0df5-4fe8-ab19-be9bc6dada9c", + "issuanceDate": "2023-03-22T11:34:05.091926539Z", + "issuer": "did:test:abc", + "proof": { + "created": "2024-12-03T16:00:21.446133-05:00", + "proofPurpose": "authentication", + "type": "Ed25519Signature2018", + "verificationMethod": "did:test:abc#key1" + }, + "type": [ + "VerifiableCredential", + "StatusList2021Credential" + ] }` ) func TestService_HandleEvent(t *testing.T) { - profile := getTestProfile(vc.StatusList2021VCStatus) loader := testutil.DocumentLoader(t) ctx := context.Background() - mockProfileSrv := NewMockProfileService(gomock.NewController(t)) - mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) - mockKMSRegistry := NewMockKMSRegistry(gomock.NewController(t)) - mockKMSRegistry.EXPECT().GetKeyManager(gomock.Any()).AnyTimes().Return(&vcskms.MockKMS{}, nil) - crypto := vccrypto.New( - &vdrmock.VDRegistry{ResolveValue: createDIDDoc("did:test:abc")}, loader) t.Run("OK", func(t *testing.T) { - cslStore := newMockCSLVCStore() - var cslWrapper *credentialstatus.CSLVCWrapper err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) require.NoError(t, err) cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) - require.NoError(t, err) + cslService := NewMockCSLService(gomock.NewController(t)) + cslService.EXPECT().GetCSLVCWrapper(gomock.Any(), cslURL).Return(cslWrapper, nil).AnyTimes() + cslService.EXPECT().SignCSL(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(profileID, profileVersion string, csl *verifiable.Credential) ([]byte, error) { + cslBytes, e := json.Marshal(csl) + require.NoError(t, e) + + getVerifiedCSL(t, cslBytes, loader, statusBytePositionIndex, true) + + return []byte(signedCSL), nil + }, + ) + cslService.EXPECT().UpsertCSLVCWrapper(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) event := createStatusUpdatedEvent( t, cslURL, profileID, profileVersion, statusBytePositionIndex, true) s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, + CSLService: cslService, }) err = s.HandleEvent(ctx, event) require.NoError(t, err) - - cslWrapper, err = cslStore.Get(ctx, cslURL) - require.NoError(t, err) - getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, true) }) t.Run("OK invalid event type", func(t *testing.T) { - cslStore := newMockCSLVCStore() - var cslWrapper *credentialstatus.CSLVCWrapper err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) require.NoError(t, err) cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) - require.NoError(t, err) + cslService := NewMockCSLService(gomock.NewController(t)) event := createStatusUpdatedEvent( t, cslURL, profileID, profileVersion, statusBytePositionIndex, true) @@ -200,74 +149,59 @@ func TestService_HandleEvent(t *testing.T) { event.Type = spi.IssuerOIDCInteractionInitiated s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, + CSLService: cslService, }) err = s.HandleEvent(ctx, event) require.NoError(t, err) - - cslWrapper, err = cslStore.Get(ctx, cslURL) - require.NoError(t, err) - cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) }) - t.Run("Error invalid event payload", func(t *testing.T) { - cslStore := newMockCSLVCStore() + t.Run("Error invalid event payload", func(t *testing.T) { var cslWrapper *credentialstatus.CSLVCWrapper err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) require.NoError(t, err) cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) - require.NoError(t, err) + cslService := NewMockCSLService(gomock.NewController(t)) + cslService.EXPECT().GetCSLVCWrapper(gomock.Any(), gomock.Any()).Return(cslWrapper, nil).AnyTimes() event := createStatusUpdatedEvent( t, cslURL, profileID, profileVersion, statusBytePositionIndex, true) - event.Data = map[string]interface{}{} + event.Data = []byte("invalid") s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, + CSLService: cslService, }) err = s.HandleEvent(ctx, event) require.Error(t, err) - - cslWrapper, err = cslStore.Get(ctx, cslURL) - require.NoError(t, err) - cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) }) } func TestService_handleEventPayload(t *testing.T) { - profile := getTestProfile(vc.StatusList2021VCStatus) loader := testutil.DocumentLoader(t) ctx := context.Background() - mockProfileSrv := NewMockProfileService(gomock.NewController(t)) - mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) - mockKMSRegistry := NewMockKMSRegistry(gomock.NewController(t)) - mockKMSRegistry.EXPECT().GetKeyManager(gomock.Any()).AnyTimes().Return(&vcskms.MockKMS{}, nil) - crypto := vccrypto.New( - &vdrmock.VDRegistry{ResolveValue: createDIDDoc("did:test:abc")}, loader) t.Run("OK", func(t *testing.T) { - cslStore := newMockCSLVCStore() - var cslWrapper *credentialstatus.CSLVCWrapper err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) require.NoError(t, err) cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) - require.NoError(t, err) + cslService := NewMockCSLService(gomock.NewController(t)) + cslService.EXPECT().GetCSLVCWrapper(gomock.Any(), cslURL).Return(cslWrapper, nil).AnyTimes() + cslService.EXPECT().SignCSL(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(profileID, profileVersion string, csl *verifiable.Credential) ([]byte, error) { + cslBytes, e := json.Marshal(csl) + require.NoError(t, e) + + getVerifiedCSL(t, cslBytes, loader, statusBytePositionIndex, true) + + return []byte(signedCSL), nil + }, + ) + cslService.EXPECT().UpsertCSLVCWrapper(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) eventPayload := credentialstatus.UpdateCredentialStatusEventPayload{ CSLURL: cslURL, @@ -277,29 +211,22 @@ func TestService_handleEventPayload(t *testing.T) { } s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, + CSLService: cslService, }) err = s.handleEventPayload(ctx, eventPayload) require.NoError(t, err) - - cslWrapper, err = cslStore.Get(ctx, cslURL) - require.NoError(t, err) - getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, true) }) t.Run("Error getCSLWrapper", func(t *testing.T) { - cslStore := newMockCSLVCStore() - var cslWrapper *credentialstatus.CSLVCWrapper err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) require.NoError(t, err) cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) + cslService := NewMockCSLService(gomock.NewController(t)) + cslService.EXPECT().GetCSLVCWrapper(gomock.Any(), cslURL).Return(nil, errors.New("getCSLWrapper error")) + eventPayload := credentialstatus.UpdateCredentialStatusEventPayload{ CSLURL: cslURL, ProfileID: profileID, @@ -308,11 +235,7 @@ func TestService_handleEventPayload(t *testing.T) { } s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, + CSLService: cslService, }) err = s.handleEventPayload(ctx, eventPayload) @@ -321,8 +244,6 @@ func TestService_handleEventPayload(t *testing.T) { }) t.Run("Error bitstring.DecodeBits", func(t *testing.T) { - cslStore := newMockCSLVCStore() - var cslWrapper *credentialstatus.CSLVCWrapper err := json.Unmarshal([]byte(cslWrapperBytesInvalidEncodedList), &cslWrapper) require.NoError(t, err) @@ -332,8 +253,8 @@ func TestService_handleEventPayload(t *testing.T) { verifiable.WithJSONLDDocumentLoader(loader)) require.NoError(t, err) - err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) - require.NoError(t, err) + cslService := NewMockCSLService(gomock.NewController(t)) + cslService.EXPECT().GetCSLVCWrapper(gomock.Any(), cslURL).Return(cslWrapper, nil).AnyTimes() eventPayload := credentialstatus.UpdateCredentialStatusEventPayload{ CSLURL: cslURL, @@ -343,31 +264,22 @@ func TestService_handleEventPayload(t *testing.T) { } s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, + CSLService: cslService, }) err = s.handleEventPayload(ctx, eventPayload) require.Error(t, err) require.ErrorContains(t, err, "failed to update status: failed to decode encodedList") - - cslWrapper, err = cslStore.Get(ctx, cslURL) - require.NoError(t, err) }) t.Run("Error bitString.Set failed", func(t *testing.T) { - cslStore := newMockCSLVCStore() - var cslWrapper *credentialstatus.CSLVCWrapper err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) require.NoError(t, err) cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) - require.NoError(t, err) + cslService := NewMockCSLService(gomock.NewController(t)) + cslService.EXPECT().GetCSLVCWrapper(gomock.Any(), cslURL).Return(cslWrapper, nil).AnyTimes() eventPayload := credentialstatus.UpdateCredentialStatusEventPayload{ CSLURL: cslURL, @@ -377,34 +289,23 @@ func TestService_handleEventPayload(t *testing.T) { } s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, + CSLService: cslService, }) err = s.handleEventPayload(ctx, eventPayload) require.Error(t, err) require.ErrorContains(t, err, "bitString.Set failed") - - cslWrapper, err = cslStore.Get(ctx, cslURL) - require.NoError(t, err) - cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) }) t.Run("Error failed to sign CSL", func(t *testing.T) { - mockProfileSrvErr := NewMockProfileService(gomock.NewController(t)) - mockProfileSrvErr.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(nil, errors.New("some error")) - cslStore := newMockCSLVCStore() - var cslWrapper *credentialstatus.CSLVCWrapper err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) require.NoError(t, err) cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) - require.NoError(t, err) + cslService := NewMockCSLService(gomock.NewController(t)) + cslService.EXPECT().GetCSLVCWrapper(gomock.Any(), cslURL).Return(cslWrapper, nil).AnyTimes() + cslService.EXPECT().SignCSL(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("failed to sign CSL")) eventPayload := credentialstatus.UpdateCredentialStatusEventPayload{ CSLURL: cslURL, @@ -414,32 +315,25 @@ func TestService_handleEventPayload(t *testing.T) { } s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrvErr, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, + CSLService: cslService, }) err = s.handleEventPayload(ctx, eventPayload) require.Error(t, err) require.ErrorContains(t, err, "failed to sign CSL") - - cslWrapper, err = cslStore.Get(ctx, cslURL) - require.NoError(t, err) - cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) }) t.Run("Error cslStore.Upsert failed", func(t *testing.T) { - cslStore := newMockCSLVCStore() - var cslWrapper *credentialstatus.CSLVCWrapper err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) require.NoError(t, err) cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) - require.NoError(t, err) + cslService := NewMockCSLService(gomock.NewController(t)) + cslService.EXPECT().GetCSLVCWrapper(gomock.Any(), cslURL).Return(cslWrapper, nil).AnyTimes() + cslService.EXPECT().SignCSL(gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte(signedCSL), nil) + cslService.EXPECT().UpsertCSLVCWrapper(gomock.Any(), gomock.Any(), gomock.Any()). + Return(errors.New("cslStore.Upsert failed")) eventPayload := credentialstatus.UpdateCredentialStatusEventPayload{ CSLURL: cslURL, @@ -449,303 +343,12 @@ func TestService_handleEventPayload(t *testing.T) { } s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, + CSLService: cslService, }) - cslStore.createErr = errors.New("some error") - err = s.handleEventPayload(ctx, eventPayload) require.Error(t, err) require.ErrorContains(t, err, "cslStore.Upsert failed") - - cslWrapper, err = cslStore.Get(ctx, cslURL) - require.NoError(t, err) - cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - }) -} - -func TestService_signCSL(t *testing.T) { - loader := testutil.DocumentLoader(t) - ctx := context.Background() - mockKMSRegistry := NewMockKMSRegistry(gomock.NewController(t)) - mockKMSRegistry.EXPECT().GetKeyManager(gomock.Any()).AnyTimes().Return(&vcskms.MockKMS{}, nil) - crypto := vccrypto.New( - &vdrmock.VDRegistry{ResolveValue: createDIDDoc("did:test:abc")}, loader) - - t.Run("OK", func(t *testing.T) { - profile := getTestProfile(vc.StatusList2021VCStatus) - - mockProfileSrv := NewMockProfileService(gomock.NewController(t)) - mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) - - cslStore := newMockCSLVCStore() - - var cslWrapper *credentialstatus.CSLVCWrapper - err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) - require.NoError(t, err) - cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - - err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) - require.NoError(t, err) - - s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, - }) - - signedCSL, err := s.signCSL(profileID, profileVersion, cslWrapper.VC) - require.NoError(t, err) - require.NotEmpty(t, signedCSL) - cslWrapper.VC = getVerifiedCSL(t, signedCSL, loader, statusBytePositionIndex, false) - require.NotEmpty(t, cslWrapper.VC.Proofs) - }) - - t.Run("BitstringStatusList -> OK", func(t *testing.T) { - profile := getTestProfile(vc.BitstringStatusList) - profile.VCConfig.DataIntegrityProof = vc.DataIntegrityProofConfig{ - Enable: true, - SuiteType: eddsa2022.SuiteType, - } - - mockProfileSrv := NewMockProfileService(gomock.NewController(t)) - mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) - - cslStore := newMockCSLVCStore() - - var cslWrapper *credentialstatus.CSLVCWrapper - err := json.Unmarshal([]byte(cslWrapperBitstringBytes), &cslWrapper) - require.NoError(t, err) - cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - - err = cslStore.Upsert(ctx, cslWrapper.VC.Contents().ID, cslWrapper) - require.NoError(t, err) - - s := New(&Config{ - DocumentLoader: loader, - CSLVCStore: cslStore, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: crypto, - }) - - signedCSL, err := s.signCSL(profileID, profileVersion, cslWrapper.VC) - require.NoError(t, err) - require.NotEmpty(t, signedCSL) - cslWrapper.VC = getVerifiedCSL(t, signedCSL, loader, statusBytePositionIndex, false) - require.NotEmpty(t, cslWrapper.VC.Proofs) - }) - - t.Run("Error failed to get profile", func(t *testing.T) { - mockProfileSrvErr := NewMockProfileService(gomock.NewController(t)) - mockProfileSrvErr.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(nil, errors.New("some error")) - s := New(&Config{ - ProfileService: mockProfileSrvErr, - }) - - signedCSL, err := s.signCSL(profileID, profileVersion, nil) - require.Empty(t, signedCSL) - require.Error(t, err) - require.ErrorContains(t, err, "failed to get profile") - }) - - t.Run("Error failed to get KMS", func(t *testing.T) { - profile := getTestProfile(vc.StatusList2021VCStatus) - - mockProfileSrv := NewMockProfileService(gomock.NewController(t)) - mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) - - mockKMSRegistryErr := NewMockKMSRegistry(gomock.NewController(t)) - mockKMSRegistryErr.EXPECT().GetKeyManager(gomock.Any()).AnyTimes().Return(nil, errors.New("some error")) - s := New(&Config{ - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistryErr, - }) - - signedCSL, err := s.signCSL(profileID, profileVersion, nil) - require.Empty(t, signedCSL) - require.Error(t, err) - require.ErrorContains(t, err, "failed to get KMS") - }) - - t.Run("Error prepareSigningOpts failed", func(t *testing.T) { - profile := getTestProfile(vc.StatusList2021VCStatus) - - mockProfileSrv := NewMockProfileService(gomock.NewController(t)) - mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) - - var cslWrapper *credentialstatus.CSLVCWrapper - err := json.Unmarshal([]byte(cslWrapperBytesInvalidProof), &cslWrapper) - require.NoError(t, err) - - cslWrapper.VC, err = verifiable.ParseCredential(cslWrapper.VCByte, - verifiable.WithDisabledProofCheck(), - verifiable.WithCredDisableValidation(), - verifiable.WithJSONLDDocumentLoader(loader)) - require.NoError(t, err) - - s := New(&Config{ - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - }) - - signedCSL, err := s.signCSL(profileID, profileVersion, cslWrapper.VC) - require.Empty(t, signedCSL) - require.Error(t, err) - require.ErrorContains(t, err, "prepareSigningOpts failed") - }) - - t.Run("Error sign CSL failed", func(t *testing.T) { - profile := getTestProfile(vc.StatusList2021VCStatus) - - mockProfileSrv := NewMockProfileService(gomock.NewController(t)) - mockProfileSrv.EXPECT().GetProfile(gomock.Any(), gomock.Any()).AnyTimes().Return(profile, nil) - - cryptoErr := vccrypto.New( - &vdrmock.VDRegistry{ResolveErr: errors.New("some error")}, loader) - var cslWrapper *credentialstatus.CSLVCWrapper - err := json.Unmarshal([]byte(cslWrapperBytes), &cslWrapper) - require.NoError(t, err) - cslWrapper.VC = getVerifiedCSL(t, cslWrapper.VCByte, loader, statusBytePositionIndex, false) - - s := New(&Config{ - DocumentLoader: loader, - ProfileService: mockProfileSrv, - KMSRegistry: mockKMSRegistry, - Crypto: cryptoErr, - }) - - signedCSL, err := s.signCSL(profileID, profileVersion, cslWrapper.VC) - require.Empty(t, signedCSL) - require.Error(t, err) - require.ErrorContains(t, err, "sign CSL failed") - }) -} - -func TestPrepareSigningOpts(t *testing.T) { - t.Parallel() - - t.Run("prepare signing opts", func(t *testing.T) { - profile := &vc.Signer{ - Creator: "did:creator#key-1", - } - - tests := []struct { - name string - proof string - result int - count int - err string - }{ - { - name: "prepare proofvalue signing opts", - proof: `{ - "created": "2020-04-17T04:17:48Z", - "proofPurpose": "assertionMethod", - "proofValue": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8AUdCJDRptPkBuqAQ", - "type": "Ed25519Signature2018", - "verificationMethod": "did:trustbloc:testnet.trustbloc.local#key-1" - }`, - }, - { - name: "prepare jws signing opts", - proof: `{ - "created": "2020-04-17T04:17:48Z", - "proofPurpose": "assertionMethod", - "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", - "type": "Ed25519Signature2018", - "verificationMethod": "did:creator#key-1" - }`, - count: 3, - }, - { - name: "prepare signing opts from proof with 3 required properties", - proof: `{ - "created": "2020-04-17T04:17:48Z", - "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", - "type": "Ed25519Signature2018", - "verificationMethod": "did:example:EiABBmUZ7JjpKSTNGq9Q==#key-1" - }`, - }, - { - name: "prepare signing opts from proof with 2 required properties", - proof: `{ - "created": "2020-04-17T04:17:48Z", - "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", - "verificationMethod": "did:example:EiABBmUZ7JjpKSTNGq9Q==#key-1" - }`, - }, - { - name: "prepare signing opts from proof with 1 required property", - proof: `{ - "created": "2020-04-17T04:17:48Z", - "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ" - }`, - }, - { - name: "prepare jws signing opts - invalid purpose", - proof: `{ - "created": "2020-04-17T04:17:48Z", - "proofPurpose": {}, - "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", - "type": "Ed25519Signature2018", - "verificationMethod": "did:example:EiABBmUZ7JjpKSTNGq9Q==#key-1" - }`, - err: "invalid 'proofPurpose' type", - }, - { - name: "prepare jws signing opts - invalid signature type", - proof: `{ - "created": "2020-04-17T04:17:48Z", - "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", - "type": {}, - "verificationMethod": "did:example:EiABBmUZ7JjpKSTNGq9Q==#key-1" - }`, - err: "invalid 'type' type", - }, - { - name: "prepare jws signing opts - invalid signature type", - proof: `{ - "created": "2020-04-17T04:17:48Z", - "jws": "CAQJKqd0MELydkNdPh7TIwgKhcMt_ypQd8ejsNbHZCJDRptPkBuqAQ", - "type": {}, - "verificationMethod": {} - }`, - err: "invalid 'verificationMethod' type", - }, - } - - t.Parallel() - - for _, test := range tests { - tc := test - t.Run(tc.name, func(t *testing.T) { - var proof map[string]interface{} - err := json.Unmarshal([]byte(tc.proof), &proof) - require.NoError(t, err) - - opts, err := prepareSigningOpts(profile, []verifiable.Proof{proof}) - - if tc.err != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tc.err) - return - } - - if tc.count > 0 { - require.Len(t, opts, tc.count) - } - - require.NoError(t, err) - require.NotEmpty(t, opts) - }) - } }) } @@ -802,110 +405,3 @@ func createStatusUpdatedEvent( spi.CredentialStatusStatusUpdated, payload) } - -type mockCSLVCStore struct { - createErr error - getCSLErr error - findErr error - s map[string]*credentialstatus.CSLVCWrapper -} - -func newMockCSLVCStore() *mockCSLVCStore { - s := &mockCSLVCStore{ - s: map[string]*credentialstatus.CSLVCWrapper{}, - } - - return s -} - -func (m *mockCSLVCStore) GetCSLURL(issuerURL, issuerID string, listID credentialstatus.ListID) (string, error) { - if m.getCSLErr != nil { - return "", m.getCSLErr - } - - return url.JoinPath(issuerURL, "issuer/profiles", issuerID, "credentials/status", string(listID)) -} - -func (m *mockCSLVCStore) Upsert(_ context.Context, cslURL string, cslWrapper *credentialstatus.CSLVCWrapper) error { - if m.createErr != nil { - return m.createErr - } - - m.s[cslURL] = cslWrapper - - return nil -} - -func (m *mockCSLVCStore) Get(_ context.Context, cslURL string) (*credentialstatus.CSLVCWrapper, error) { - if m.findErr != nil { - return nil, m.findErr - } - - w, ok := m.s[cslURL] - if !ok { - return nil, credentialstatus.ErrDataNotFound - } - - return w, nil -} - -func getTestProfile(statusType vc.StatusType) *profileapi.Issuer { - return &profileapi.Issuer{ - ID: profileID, - Name: "testprofile", - GroupID: "externalID", - VCConfig: &profileapi.VCConfig{ - Format: vcsverifiable.Ldp, - SigningAlgorithm: "Ed25519Signature2018", - KeyType: kms.ED25519Type, - Status: profileapi.StatusConfig{ - Type: statusType, - }, - }, - SigningDID: &profileapi.SigningDID{ - DID: "did:test:abc", - Creator: "did:test:abc#key1", - }, - } -} - -func createDIDDoc(didID string) *did.Doc { - const ( - didContext = "https://w3id.org/did/v1" - keyType = "Ed25519VerificationKey2018" - ) - - pubKey, _, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - panic(err) - } - - creator := didID + "#key1" - - service := did.Service{ - ID: "did:example:123456789abcdefghi#did-communication", - RecipientKeys: []string{creator}, - Priority: 0, - } - - signingKey := did.VerificationMethod{ - ID: creator, - Type: keyType, - Controller: didID, - Value: pubKey, - } - - createdTime := time.Now() - - return &did.Doc{ - Context: []string{didContext}, - ID: didID, - VerificationMethod: []did.VerificationMethod{signingKey}, - Service: []did.Service{service}, - Created: &createdTime, - AssertionMethod: []did.Verification{{VerificationMethod: signingKey}}, - Authentication: []did.Verification{{VerificationMethod: signingKey}}, - CapabilityInvocation: []did.Verification{{VerificationMethod: signingKey}}, - CapabilityDelegation: []did.Verification{{VerificationMethod: signingKey}}, - } -}