diff --git a/component/profile/reader/file/reader.go b/component/profile/reader/file/reader.go index 71856cb33..0b3e125cc 100644 --- a/component/profile/reader/file/reader.go +++ b/component/profile/reader/file/reader.go @@ -208,23 +208,25 @@ func NewVerifierReader(config *Config) (*VerifierReader, error) { return &r, nil } -func (p *VerifierReader) setTrustList(verifier *profileapi.Verifier) { +func (p *VerifierReader) setTrustList( + verifier *profileapi.Verifier, +) { if verifier == nil || verifier.Checks == nil || len(createdIssuers) == 0 || len(verifier.Checks.Credential.IssuerTrustList) == 0 { return } - var updatedList []string - for _, item := range verifier.Checks.Credential.IssuerTrustList { - issuer, ok := createdIssuers[item] + updated := make(map[string]profileapi.TrustList) + for k, v := range verifier.Checks.Credential.IssuerTrustList { + issuer, ok := createdIssuers[k] if !ok { continue } - updatedList = append(updatedList, issuer.SigningDID.DID) + updated[issuer.SigningDID.DID] = v } - verifier.Checks.Credential.IssuerTrustList = updatedList + verifier.Checks.Credential.IssuerTrustList = updated } // GetProfile returns profile with given id. diff --git a/pkg/profile/api.go b/pkg/profile/api.go index 3190f8be2..83eb0ac1b 100644 --- a/pkg/profile/api.go +++ b/pkg/profile/api.go @@ -176,7 +176,12 @@ type CredentialChecks struct { CredentialExpiry bool `json:"credentialExpiry,omitempty"` Strict bool `json:"strict,omitempty"` LinkedDomain bool `json:"linkedDomain,omitempty"` - IssuerTrustList []string `json:"issuerTrustList,omitempty"` + IssuerTrustList map[string]TrustList `json:"issuerTrustList,omitempty"` +} + +// TrustList contains list of configuration that verifier is trusted to accept. +type TrustList struct { + CredentialTypes []string `json:"credentialTypes,omitempty"` } // SigningDID contains information about profile signing did. diff --git a/pkg/service/verifypresentation/verifypresentation_service.go b/pkg/service/verifypresentation/verifypresentation_service.go index bbffa04e4..d36ee5b78 100644 --- a/pkg/service/verifypresentation/verifypresentation_service.go +++ b/pkg/service/verifypresentation/verifypresentation_service.go @@ -253,22 +253,49 @@ func (s *Service) checkCredentialExpiry(ctx context.Context, lazy []*LazyCredent return nil } -func (s *Service) checkIssuerTrustList(_ context.Context, lazy []*LazyCredential, trustList []string) error { +func (s *Service) checkIssuerTrustList( + _ context.Context, + lazy []*LazyCredential, + trustList map[string]profileapi.TrustList, +) error { for _, input := range lazy { var issuerID string + var credTypes []string + switch cred := input.Raw().(type) { case *verifiable.Credential: issuerID = cred.Issuer.ID + credTypes = cred.Types case map[string]interface{}: issuerID = fmt.Sprint(cred["issuer"]) + cr, ok := cred["type"].([]interface{}) + if ok { + for _, t := range cr { + credTypes = append(credTypes, fmt.Sprint(t)) + } + } default: return fmt.Errorf("can not validate issuer trust list. unexpected type %v", reflect.TypeOf(input.Raw()).String()) } - if !lo.Contains(trustList, issuerID) { + var finalCredType string + if len(credTypes) > 0 { + finalCredType = credTypes[len(credTypes)-1] + } + + cfg, ok := trustList[issuerID] + if !ok { return fmt.Errorf("issuer with id: %v is not a member of trustlist", issuerID) } + + if len(cfg.CredentialTypes) == 0 { // we are trusting to all credential types + continue + } + + if !lo.Contains(cfg.CredentialTypes, finalCredType) { + return fmt.Errorf("credential type: %v is not a member of trustlist configuration", finalCredType) + } } return nil diff --git a/pkg/service/verifypresentation/verifypresentation_service_test.go b/pkg/service/verifypresentation/verifypresentation_service_test.go index 17f8b5665..0001f6e61 100644 --- a/pkg/service/verifypresentation/verifypresentation_service_test.go +++ b/pkg/service/verifypresentation/verifypresentation_service_test.go @@ -132,7 +132,9 @@ func TestService_VerifyPresentation(t *testing.T) { Format: nil, CredentialExpiry: true, Strict: true, - IssuerTrustList: []string{"https://example.edu/issuers/14"}, + IssuerTrustList: map[string]profileapi.TrustList{ + "https://example.edu/issuers/14": {}, + }, }, }, }, @@ -144,6 +146,133 @@ func TestService_VerifyPresentation(t *testing.T) { want: nil, wantErr: false, }, + { + name: "OK with credential type", + fields: fields{ + getVDR: func() vdrapi.Registry { + return signedVPResult.VDR + }, + getVcVerifier: func() vcVerifier { + mockVerifier := NewMockVcVerifier(gomock.NewController(t)) + mockVerifier.EXPECT().ValidateCredentialProof( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any()).Times(1).Return(nil) + mockVerifier.EXPECT().ValidateVCStatus( + context.Background(), + gomock.Any(), + gomock.Any()).Times(1).Return(nil) + mockVerifier.EXPECT().ValidateLinkedDomain( + context.Background(), + gomock.Any()).Times(1).Return(nil) + return mockVerifier + }, + }, + args: args{ + getPresentation: func() *verifiable.Presentation { + return signedVPResult.Presentation + }, + profile: &profileapi.Verifier{ + SigningDID: &profileapi.SigningDID{DID: "did:key:abc"}, + Checks: &profileapi.VerificationChecks{ + Presentation: &profileapi.PresentationChecks{ + Proof: true, + Format: nil, + }, + Credential: profileapi.CredentialChecks{ + Proof: true, + Status: true, + LinkedDomain: true, + Format: nil, + CredentialExpiry: true, + Strict: true, + IssuerTrustList: map[string]profileapi.TrustList{ + "https://example.edu/issuers/14": { + CredentialTypes: []string{ + "UniversityDegreeCredential", + }, + }, + }, + }, + }, + }, + opts: &Options{ + Domain: crypto.Domain, + Challenge: crypto.Challenge, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "Err credential type not in trust list", + fields: fields{ + getVDR: func() vdrapi.Registry { + return signedVPResult.VDR + }, + getVcVerifier: func() vcVerifier { + mockVerifier := NewMockVcVerifier(gomock.NewController(t)) + mockVerifier.EXPECT().ValidateCredentialProof( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any()).Times(1).Return(nil) + mockVerifier.EXPECT().ValidateVCStatus( + context.Background(), + gomock.Any(), + gomock.Any()).Times(1).Return(nil) + mockVerifier.EXPECT().ValidateLinkedDomain( + context.Background(), + gomock.Any()).Times(1).Return(nil) + return mockVerifier + }, + }, + args: args{ + getPresentation: func() *verifiable.Presentation { + return signedVPResult.Presentation + }, + profile: &profileapi.Verifier{ + SigningDID: &profileapi.SigningDID{DID: "did:key:abc"}, + Checks: &profileapi.VerificationChecks{ + Presentation: &profileapi.PresentationChecks{ + Proof: true, + Format: nil, + }, + Credential: profileapi.CredentialChecks{ + Proof: true, + Status: true, + LinkedDomain: true, + Format: nil, + CredentialExpiry: true, + Strict: true, + IssuerTrustList: map[string]profileapi.TrustList{ + "https://example.edu/issuers/14": { + CredentialTypes: []string{ + "DrivingLicense", + }, + }, + }, + }, + }, + }, + opts: &Options{ + Domain: crypto.Domain, + Challenge: crypto.Challenge, + }, + }, + want: []PresentationVerificationCheckResult{ + { + Check: "issuerTrustList", + Error: "credential type: UniversityDegreeCredential is not a member of trustlist configuration", + }, + }, + wantErr: false, + }, { name: "OK no checks", fields: fields{ @@ -176,7 +305,6 @@ func TestService_VerifyPresentation(t *testing.T) { want: nil, wantErr: false, }, - { name: "Error credentials", fields: fields{ @@ -214,11 +342,13 @@ func TestService_VerifyPresentation(t *testing.T) { Format: nil, }, Credential: profileapi.CredentialChecks{ - Proof: true, - Status: true, - LinkedDomain: true, - Format: nil, - IssuerTrustList: []string{"random"}, + Proof: true, + Status: true, + LinkedDomain: true, + Format: nil, + IssuerTrustList: map[string]profileapi.TrustList{ + "random": {}, + }, }, }, }, @@ -823,15 +953,22 @@ func TestCredentialStrict(t *testing.T) { func TestCheckTrustList(t *testing.T) { s := New(&Config{}) - t.Run("from credentials", func(t *testing.T) { - cred := &verifiable.Credential{Issuer: verifiable.Issuer{ - ID: "123432123", - }} + t.Run("from credentials v1 trust list", func(t *testing.T) { + cred := &verifiable.Credential{ + Types: []string{ + "VerifiableCredential", + "UniversityDegreeCredential", + }, + Issuer: verifiable.Issuer{ + ID: "123432123", + }} err := s.checkIssuerTrustList( context.TODO(), []*LazyCredential{NewLazyCredential(cred)}, - []string{"a"}, + map[string]profileapi.TrustList{ + "a": {}, + }, ) assert.ErrorContains(t, err, "issuer with id: 123432123 is not a member of trustlist") @@ -843,7 +980,9 @@ func TestCheckTrustList(t *testing.T) { err := s.checkIssuerTrustList( context.TODO(), []*LazyCredential{NewLazyCredential(cred)}, - []string{"a"}, + map[string]profileapi.TrustList{ + "a": {}, + }, ) assert.ErrorContains(t, err, diff --git a/test/bdd/features/oidc4vc_api.feature b/test/bdd/features/oidc4vc_api.feature index bd3d613aa..94d9fd39d 100644 --- a/test/bdd/features/oidc4vc_api.feature +++ b/test/bdd/features/oidc4vc_api.feature @@ -57,29 +57,36 @@ Feature: OIDC4VC REST API # LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | - Scenario: OIDC credential issuance and verification Pre Auth flow with trustlist (Success) + Scenario Outline: OIDC credential issuance and verification Pre Auth flow with trustlist (Success) Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" - And Issuer with id "bank_issuer_sdjwt_v5/v1.0" is authorized as a Profile user - And User holds credential "CrudeProductCredential" with templateID "crudeProductCredentialTemplateID" + And Issuer with id "" is authorized as a Profile user + And User holds credential "" with templateID "" When User interacts with Wallet to initiate credential issuance using pre authorization code flow Then credential is issued - Then User interacts with Verifier and initiate OIDC4VP interaction under "v_myprofile_jwt_whitelist/v1.0" profile for organization "test_org" with presentation definition ID "3c8b1d9a-limit-disclosure-optional-fields" and fields "unit_of_measure_barrel,api_gravity,category,supplier_address" + Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" And Verifier from organization "test_org" retrieves interactions claims Then we wait 2 seconds And Verifier form organization "test_org" requests deleted interactions claims + Examples: + | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | + | bank_issuer_sdjwt_v5/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt_whitelist/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | + | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt_whitelist/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | # Error cases - Scenario: OIDC credential issuance and verification Pre Auth flow with trustlist (Fail) + Scenario Outline: OIDC credential issuance and verification Pre Auth flow with trustlist (Fail) Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" - And Issuer with id "bank_issuer/v1.0" is authorized as a Profile user - And User holds credential "CrudeProductCredential" with templateID "crudeProductCredentialTemplateID" + And Issuer with id "" is authorized as a Profile user + And User holds credential "" with templateID "" When User interacts with Wallet to initiate credential issuance using pre authorization code flow Then credential is issued - Then User interacts with Verifier and initiate OIDC4VP interaction under "v_myprofile_jwt_whitelist/v1.0" profile for organization "test_org" with presentation definition ID "3c8b1d9a-limit-disclosure-optional-fields" and fields "unit_of_measure_barrel,api_gravity,category,supplier_address" and receives "is not a member of trustlist" error - + Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" and receives "is not a member of trustlist" error + Examples: + | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | + | bank_issuer_sdjwt_v5/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt_whitelist/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | + | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt_whitelist/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,registration_city,commuter_classification | Scenario: OIDC credential issuance and verification Pre Auth flow (Invalid Claims) Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" diff --git a/test/bdd/fixtures/profile/profiles.json b/test/bdd/fixtures/profile/profiles.json index 69c25c8b2..b4ea20d9b 100644 --- a/test/bdd/fixtures/profile/profiles.json +++ b/test/bdd/fixtures/profile/profiles.json @@ -1930,9 +1930,14 @@ "format": [ "jwt" ], - "issuerTrustList" : [ - "bank_issuer_sdjwt_v5" - ], + "issuerTrustList": { + "bank_issuer" : {}, + "bank_issuer_sdjwt_v5": { + "credentialTypes": [ + "CrudeProductCredential" + ] + } + }, "proof": true, "status": true, "strict": true