diff --git a/config/configClient.go b/config/configClient.go index 96f25d38..dc3bcf5f 100644 --- a/config/configClient.go +++ b/config/configClient.go @@ -12,7 +12,6 @@ import ( const SERVICES_PATH = "service" - const SERVICE_DEFAULT_SCOPE = "" var ErrorCcsNoResponse = errors.New("no_response_from_ccs") @@ -53,6 +52,15 @@ type Credential struct { TrustedParticipantsLists []string `json:"trustedParticipantsLists,omitempty" mapstructure:"trustedParticipantsLists,omitempty"` // A list of (EBSI Trusted Issuers Registry compatible) endpoints to retrieve the trusted issuers from. The attributes need to be formated to comply with the verifiers requirements. TrustedIssuersLists []string `json:"trustedIssuersLists,omitempty" mapstructure:"trustedIssuersLists,omitempty"` + // Configuration of Holder Verfification + HolderVerification HolderVerification `json:"holderVerification" mapstructure:"holderVerification"` +} + +type HolderVerification struct { + // should holder verification be enabled + Enabled bool `json:"enabled" mapstructure:"enabled"` + // the claim containing the holder + Claim string `json:"claim" mapstructure:"claim"` } func (cs ConfiguredService) GetRequiredCredentialTypes(scope string) []string { diff --git a/config/configClient_test.go b/config/configClient_test.go index 1b958c79..9e9d70f4 100644 --- a/config/configClient_test.go +++ b/config/configClient_test.go @@ -36,15 +36,16 @@ func Test_getServices(t *testing.T) { } assert.NotEmpty(t, services) expectedData := []ConfiguredService{ - ConfiguredService{ + { Id: "service_all", DefaultOidcScope: "did_write", ServiceScopes: map[string][]Credential{ - "did_write": []Credential{ - Credential{ + "did_write": { + { Type: "VerifiableCredential", TrustedParticipantsLists: []string{"https://tir-pdc.gaia-x.fiware.dev"}, TrustedIssuersLists: []string{"https://til-pdc.gaia-x.fiware.dev"}, + HolderVerification: HolderVerification{Enabled: false, Claim: "subject"}, }, }, }, diff --git a/config/data/ccs_full.json b/config/data/ccs_full.json index 216970f1..b8585982 100644 --- a/config/data/ccs_full.json +++ b/config/data/ccs_full.json @@ -15,7 +15,11 @@ ], "trustedIssuersLists": [ "https://til-pdc.gaia-x.fiware.dev" - ] + ], + "holderVerification": { + "enabled": false, + "claim": "subject" + } } ] } diff --git a/verifier/credentialsConfig.go b/verifier/credentialsConfig.go index 5b6b56f0..43adf2db 100644 --- a/verifier/credentialsConfig.go +++ b/verifier/credentialsConfig.go @@ -31,6 +31,8 @@ type CredentialsConfig interface { GetTrustedIssuersLists(serviceIdentifier string, scope string, credentialType string) (trustedIssuersRegistryUrl []string, err error) // The credential types that are required for the given service and scope RequiredCredentialTypes(serviceIdentifier string, scope string) (credentialTypes []string, err error) + // Get holder verification + GetHolderVerification(serviceIdentifier string, scope string, credentialType string) (isEnabled bool, holderClaim string, err error) } type ServiceBackedCredentialsConfig struct { @@ -159,3 +161,17 @@ func (cc ServiceBackedCredentialsConfig) GetTrustedIssuersLists(serviceIdentifie logging.Log().Debugf("No trusted issuers for %s - %s", serviceIdentifier, credentialType) return []string{}, nil } + +func (cc ServiceBackedCredentialsConfig) GetHolderVerification(serviceIdentifier string, scope string, credentialType string) (isEnabled bool, holderClaim string, err error) { + logging.Log().Debugf("Get holder verification for %s - %s - %s.", serviceIdentifier, scope, credentialType) + cacheEntry, hit := common.GlobalCache.ServiceCache.Get(serviceIdentifier) + if hit { + credential, ok := cacheEntry.(config.ConfiguredService).GetCredential(scope, credentialType) + if ok { + logging.Log().Debugf("Found holder verification %v:%s for %s - %s", credential.HolderVerification.Enabled, credential.HolderVerification.Claim, serviceIdentifier, credentialType) + return credential.HolderVerification.Enabled, credential.HolderVerification.Claim, nil + } + } + logging.Log().Debugf("No holder verification for %s - %s", serviceIdentifier, credentialType) + return false, "", nil +} diff --git a/verifier/holder.go b/verifier/holder.go new file mode 100644 index 00000000..6ec1dd51 --- /dev/null +++ b/verifier/holder.go @@ -0,0 +1,34 @@ +package verifier + +import ( + "strings" + + "github.com/fiware/VCVerifier/logging" + "github.com/trustbloc/vc-go/verifiable" +) + +type HolderValidationService struct{} + +func (hvs *HolderValidationService) ValidateVC(verifiableCredential *verifiable.Credential, validationContext ValidationContext) (result bool, err error) { + logging.Log().Debugf("Validate holder for %s", logging.PrettyPrintObject(verifiableCredential)) + defer func() { + if recErr := recover(); recErr != nil { + logging.Log().Warnf("Was not able to convert context. Err: %v", recErr) + err = ErrorCannotConverContext + } + }() + holderContext := validationContext.(HolderValidationContext) + + path := strings.Split(holderContext.claim, ".") + pathLength := len(path) + + credentialJson := verifiableCredential.ToRawJSON() + currentClaim := credentialJson["credentialSubject"].(map[string]interface{}) + for i, p := range path { + if i == pathLength-1 { + return currentClaim[p].(string) == holderContext.holder, err + } + currentClaim = currentClaim[p].(verifiable.JSONObject) + } + return false, err +} diff --git a/verifier/holder_test.go b/verifier/holder_test.go new file mode 100644 index 00000000..6c97e2c3 --- /dev/null +++ b/verifier/holder_test.go @@ -0,0 +1,63 @@ +package verifier + +import ( + "testing" + + "github.com/fiware/VCVerifier/logging" + "github.com/trustbloc/vc-go/verifiable" +) + +func TestValidateVC(t *testing.T) { + + type test struct { + testName string + credentialToVerifiy verifiable.Credential + validationContext ValidationContext + expectedResult bool + } + tests := []test{ + {testName: "If the holder is correct, the vc should be allowed.", credentialToVerifiy: getCredentialWithHolder("subject", "holder"), validationContext: HolderValidationContext{claim: "subject", holder: "holder"}, expectedResult: true}, + {testName: "If the holder is correct inside the sub element, the vc should be allowed.", credentialToVerifiy: getCredentialWithHolderInSubelement("holder"), validationContext: HolderValidationContext{claim: "sub.holder", holder: "holder"}, expectedResult: true}, + {testName: "If the holder is not correct, the vc should be rejected.", credentialToVerifiy: getCredentialWithHolder("subject", "holder"), validationContext: HolderValidationContext{claim: "subject", holder: "someOneElse"}, expectedResult: false}, + {testName: "If the holder is not correct inside the sub element, the vc should be rejected.", credentialToVerifiy: getCredentialWithHolderInSubelement("holder"), validationContext: HolderValidationContext{claim: "sub.holder", holder: "someOneElse"}, expectedResult: false}, + } + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + + logging.Log().Info("TestValidateVC +++++++++++++++++ Running test: ", tc.testName) + + holderValidationService := HolderValidationService{} + + result, _ := holderValidationService.ValidateVC(&tc.credentialToVerifiy, tc.validationContext) + if result != tc.expectedResult { + t.Errorf("%s - Expected result %v but was %v.", tc.testName, tc.expectedResult, result) + return + } + }) + } +} + +func getCredentialWithHolder(holderClaim, holder string) verifiable.Credential { + vc, _ := verifiable.CreateCredential(verifiable.CredentialContents{ + Issuer: &verifiable.Issuer{ID: "did:test:issuer"}, + Types: []string{"VerifiableCredential"}, + Subject: []verifiable.Subject{ + { + CustomFields: map[string]interface{}{holderClaim: holder}, + }, + }}, verifiable.CustomFields{}) + return *vc +} + +func getCredentialWithHolderInSubelement(holder string) verifiable.Credential { + + vc, _ := verifiable.CreateCredential(verifiable.CredentialContents{ + Issuer: &verifiable.Issuer{ID: "did:test:issuer"}, + Types: []string{"VerifiableCredential"}, + Subject: []verifiable.Subject{ + { + CustomFields: map[string]interface{}{"sub": map[string]interface{}{"holder": holder}}, + }, + }}, verifiable.CustomFields{}) + return *vc +} diff --git a/verifier/verifier.go b/verifier/verifier.go index 83666e69..024e527f 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -127,6 +127,19 @@ func (trvc TrustRegistriesValidationContext) GetRequiredCredentialTypes() []stri return removeDuplicate(requiredTypes) } +type HolderValidationContext struct { + claim string + holder string +} + +func (hvc HolderValidationContext) GetClaim() string { + return hvc.claim +} + +func (hvc HolderValidationContext) GetHolder() string { + return hvc.holder +} + func removeDuplicate[T string | int](sliceList []T) []T { allKeys := make(map[T]bool) list := []T{} @@ -367,6 +380,7 @@ func (v *CredentialVerifier) GenerateToken(clientId, subject, audience string, s // collect all submitted credential types credentialsByType := map[string][]*verifiable.Credential{} credentialTypes := []string{} + holder := verifiablePresentation.Holder for _, vc := range verifiablePresentation.Credentials() { for _, credentialType := range vc.Contents().Types { if _, ok := credentialsByType[credentialType]; !ok { @@ -392,6 +406,23 @@ func (v *CredentialVerifier) GenerateToken(clientId, subject, audience string, s } } for _, credential := range credentialsNeededForScope { + holderValidationContexts, err := v.getHolderValidationContext(clientId, scope, credentialTypes, holder) + if err != nil { + logging.Log().Warnf("Was not able to create the holder validation context. Credential will be rejected. Err: %v", err) + return 0, "", ErrorVerficationContextSetup + } + holderValidationService := HolderValidationService{} + for _, holderValidationContext := range holderValidationContexts { + result, err := holderValidationService.ValidateVC(credential, holderValidationContext) + if err != nil { + logging.Log().Warnf("Failed to verify credential %s. Err: %v", logging.PrettyPrintObject(credential), err) + return 0, "", err + } + if !result { + logging.Log().Infof("VC %s is not valid.", logging.PrettyPrintObject(credential)) + return 0, "", ErrorInvalidVC + } + } for _, verificationService := range v.validationServices { result, err := verificationService.ValidateVC(credential, verificationContext) if err != nil { @@ -523,6 +554,22 @@ func (v *CredentialVerifier) AuthenticationResponse(state string, verifiablePres } } +func (v *CredentialVerifier) getHolderValidationContext(clientId string, scope string, credentialTypes []string, holder string) (validationContext []HolderValidationContext, err error) { + validationContexts := []HolderValidationContext{} + for _, credentialType := range credentialTypes { + isEnabled, claim, err := v.credentialsConfig.GetHolderVerification(clientId, scope, credentialType) + if err != nil { + logging.Log().Warnf("Was not able to get valid holder verification config for client %s, scope %s and type %s. Err: %v", clientId, scope, credentialType, err) + return validationContext, err + } + if !isEnabled { + continue + } + validationContexts = append(validationContext, HolderValidationContext{claim: claim, holder: holder}) + } + return validationContexts, err +} + func (v *CredentialVerifier) getTrustRegistriesValidationContext(clientId string, credentialTypes []string) (verificationContext TrustRegistriesValidationContext, err error) { trustedIssuersLists := map[string][]string{} trustedParticipantsRegistries := map[string][]string{} diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index c9985d3a..96232f33 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -83,13 +83,15 @@ type mockTokenCache struct { errorToThrow error } type mockCredentialConfig struct { + // ServiceId->Scope->CredentialType-> TIR/TIL URLs - mockScopes map[string]map[string]map[string][]string + mockScopes map[string]map[string]map[string]configModel.Credential mockError error } -func createMockCredentials(serviceId, scope, credentialType, url string) map[string]map[string]map[string][]string { - return map[string]map[string]map[string][]string{serviceId: {scope: map[string][]string{credentialType: {url}}}} +func createMockCredentials(serviceId, scope, credentialType, url, holderClaim string, holderVerfication bool) map[string]map[string]map[string]configModel.Credential { + credential := configModel.Credential{TrustedParticipantsLists: []string{url}, TrustedIssuersLists: []string{url}, HolderVerification: configModel.HolderVerification{Enabled: holderVerfication, Claim: holderClaim}} + return map[string]map[string]map[string]configModel.Credential{serviceId: {scope: map[string]configModel.Credential{credentialType: credential}}} } func (mcc mockCredentialConfig) GetScope(serviceIdentifier string) (credentialTypes []string, err error) { @@ -102,13 +104,13 @@ func (mcc mockCredentialConfig) GetTrustedParticipantLists(serviceIdentifier str if mcc.mockError != nil { return trustedIssuersRegistryUrl, mcc.mockError } - return mcc.mockScopes[serviceIdentifier][scope][credentialType], err + return mcc.mockScopes[serviceIdentifier][scope][credentialType].TrustedParticipantsLists, err } func (mcc mockCredentialConfig) GetTrustedIssuersLists(serviceIdentifier string, scope string, credentialType string) (trustedIssuersRegistryUrl []string, err error) { if mcc.mockError != nil { return trustedIssuersRegistryUrl, mcc.mockError } - return mcc.mockScopes[serviceIdentifier][scope][credentialType], err + return mcc.mockScopes[serviceIdentifier][scope][credentialType].TrustedIssuersLists, err } func (mcc mockCredentialConfig) RequiredCredentialTypes(serviceIdentifier string, scope string) (credentialTypes []string, err error) { @@ -118,6 +120,14 @@ func (mcc mockCredentialConfig) RequiredCredentialTypes(serviceIdentifier string return maps.Keys(mcc.mockScopes[serviceIdentifier][scope]), err } +func (mcc mockCredentialConfig) GetHolderVerification(serviceIdentifier string, scope string, credentialType string) (isEnabled bool, holderClaim string, err error) { + if mcc.mockError != nil { + return isEnabled, holderClaim, mcc.mockError + } + credential := mcc.mockScopes[serviceIdentifier][scope][credentialType] + return credential.HolderVerification.Enabled, credential.HolderVerification.Claim, err +} + func (msc *mockSessionCache) Add(k string, x interface{}, d time.Duration) error { if msc.errorToThrow != nil { return msc.errorToThrow @@ -167,7 +177,7 @@ type siopInitTest struct { testAddress string testSessionId string testClientId string - credentialScopes map[string]map[string]map[string][]string + credentialScopes map[string]map[string]map[string]configModel.Credential mockConfigError error expectedCallback string expectedConnection string @@ -238,16 +248,16 @@ func getInitSiopTests() []siopInitTest { cacheFailError := errors.New("cache_fail") return []siopInitTest{ - {testName: "If all parameters are set, a proper connection string should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", ""), mockConfigError: nil, expectedCallback: "https://client.org/callback", + {testName: "If all parameters are set, a proper connection string should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", expectedConnection: "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, }, - {testName: "The scope should be included if configured.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "myService", credentialScopes: createMockCredentials("myService", "someScope", "org.fiware.MySpecialCredential", "some.url"), mockConfigError: nil, expectedCallback: "https://client.org/callback", + {testName: "The scope should be included if configured.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "myService", credentialScopes: createMockCredentials("myService", "someScope", "org.fiware.MySpecialCredential", "some.url", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", expectedConnection: "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce&scope=someScope", sessionCacheError: nil, expectedError: nil, }, - {testName: "If the login-session could not be cached, an error should be thrown.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", ""), mockConfigError: nil, expectedCallback: "https://client.org/callback", + {testName: "If the login-session could not be cached, an error should be thrown.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", expectedConnection: "", sessionCacheError: cacheFailError, expectedError: cacheFailError, }, - {testName: "If config service throws an error, no scope should be included.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "myService", credentialScopes: createMockCredentials("", "", "", ""), mockConfigError: errors.New("config_error"), expectedCallback: "https://client.org/callback", + {testName: "If config service throws an error, no scope should be included.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "myService", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: errors.New("config_error"), expectedCallback: "https://client.org/callback", expectedConnection: "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, }, } @@ -259,13 +269,13 @@ func TestStartSameDeviceFlow(t *testing.T) { logging.Configure(true, "DEBUG", true, []string{}) tests := []siopInitTest{ - {testName: "If everything is provided, a samedevice flow should be started.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", ""), mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", + {testName: "If everything is provided, a samedevice flow should be started.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", expectedConnection: "https://myhost.org/redirect?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://myhost.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, }, - {testName: "The scope should be included if configured.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "myService", credentialScopes: createMockCredentials("myService", "someScope", "org.fiware.MySpecialCredential", "some.url"), mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", + {testName: "The scope should be included if configured.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "myService", credentialScopes: createMockCredentials("myService", "someScope", "org.fiware.MySpecialCredential", "some.url", "", false), mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", expectedConnection: "https://myhost.org/redirect?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://myhost.org/api/v1/authentication_response&state=randomState&nonce=randomNonce&scope=someScope", sessionCacheError: nil, expectedError: nil, }, - {testName: "If the request cannot be cached, an error should be responded.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", ""), mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", + {testName: "If the request cannot be cached, an error should be responded.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", expectedConnection: "", sessionCacheError: cacheFailError, expectedError: cacheFailError, }, } @@ -670,7 +680,7 @@ type openIdProviderMetadataTest struct { host string testName string serviceIdentifier string - credentialScopes map[string]map[string]map[string][]string + credentialScopes map[string]map[string]map[string]configModel.Credential mockConfigError error expectedOpenID common.OpenIDProviderMetadata } @@ -680,12 +690,12 @@ func getOpenIdProviderMetadataTests() []openIdProviderMetadataTest { return []openIdProviderMetadataTest{ {testName: "Test OIDC metadata with existing scopes", serviceIdentifier: "serviceId", host: verifierHost, - credentialScopes: map[string]map[string]map[string][]string{"serviceId": {"Scope1": {}, "Scope2": {}}}, mockConfigError: nil, + credentialScopes: map[string]map[string]map[string]configModel.Credential{"serviceId": {"Scope1": {}, "Scope2": {}}}, mockConfigError: nil, expectedOpenID: common.OpenIDProviderMetadata{ Issuer: verifierHost, ScopesSupported: []string{"Scope1", "Scope2"}}}, {testName: "Test OIDC metadata with non-existing scopes", serviceIdentifier: "serviceId", host: verifierHost, - credentialScopes: map[string]map[string]map[string][]string{"serviceId": {}}, mockConfigError: nil, + credentialScopes: map[string]map[string]map[string]configModel.Credential{"serviceId": {}}, mockConfigError: nil, expectedOpenID: common.OpenIDProviderMetadata{ Issuer: verifierHost, ScopesSupported: []string{}}},