From d7fa18def65a479484558794a0e63bb7444df273 Mon Sep 17 00:00:00 2001 From: Miguel Ortega Date: Fri, 13 Mar 2026 12:05:25 +0100 Subject: [PATCH] fix: check all credentialsConfig for a credential type All credentialsConfig associated with a credential type are now checked. At least one of them must be satisfied (OR) --- verifier/trustedissuer.go | 69 ++++++------ verifier/trustedissuer_test.go | 190 +++++++++++++++++++++++---------- 2 files changed, 171 insertions(+), 88 deletions(-) diff --git a/verifier/trustedissuer.go b/verifier/trustedissuer.go index 07ecdce..5655289 100644 --- a/verifier/trustedissuer.go +++ b/verifier/trustedissuer.go @@ -105,14 +105,14 @@ func isWildcardTil(tilList []string) (isWildcard bool, err error) { func verifyWithCredentialsConfig(verifiableCredential *verifiable.Credential, credentials []tir.Credential) (result bool, err error) { - credentialsConfigMap := map[string]tir.Credential{} + credentialsConfigMap := map[string][]tir.Credential{} // format for better validation for _, credential := range credentials { - credentialsConfigMap[credential.CredentialsType] = credential + credentialsConfigMap[credential.CredentialsType] = append(credentialsConfigMap[credential.CredentialsType], credential) } - // initalize to true, since everything without a specific rule is considered to be allowed + // initialize to true, since everything without a specific rule is considered to be allowed var subjectAllowed = true // validate that the type(s) is allowed @@ -134,39 +134,44 @@ func verifyWithCredentialsConfig(verifiableCredential *verifiable.Credential, cr return true, err } -func verifyForType(subjectToVerfiy verifiable.Subject, credentialConfig tir.Credential) (result bool) { - for _, claim := range credentialConfig.Claims { - - if claim.Path != "" { - validClaim := verifyWithJsonPath(subjectToVerfiy, claim) - if validClaim { - logging.Log().Debugf("Claim with path %s is valid. Credential Subject %s", claim.Path, logging.PrettyPrintObject(subjectToVerfiy)) - continue +// verifyForType returns true if the subject satisfies at least one credential config (OR). +// Each config is satisfied only if all its claims are valid (AND). +func verifyForType(subjectToVerify verifiable.Subject, credentialConfig []tir.Credential) bool { + for _, config := range credentialConfig { + allClaimsValid := true + for _, claim := range config.Claims { + if claim.Path != "" { + if !verifyWithJsonPath(subjectToVerify, claim) { + logging.Log().Warnf("Claim with path %s is not valid.", claim.Path) + allClaimsValid = false + break + } + logging.Log().Debugf("Claim with path %s is valid. Credential Subject %s", claim.Path, logging.PrettyPrintObject(subjectToVerify)) } else { - logging.Log().Warnf("Claim with path %s is not valid.", claim.Path) - return false - } - } else { - // old name base logic - claimValue, exists := subjectToVerfiy.CustomFields[claim.Name] - if !exists { - logging.Log().Debugf("Restricted claim %s is not part of the subject %s.", claim.Name, logging.PrettyPrintObject(subjectToVerfiy)) - continue - } - isAllowed := contains(claim.AllowedValues, claimValue) - if !isAllowed { - logging.Log().Debugf("The claim value %s is not allowed by the config %s.", logging.PrettyPrintObject(claimValue), logging.PrettyPrintObject(credentialConfig)) - return false + // legacy name-based validation + claimValue, exists := subjectToVerify.CustomFields[claim.Name] + if !exists { + logging.Log().Debugf("Claim %s is not present in subject %s, skipping.", claim.Name, logging.PrettyPrintObject(subjectToVerify)) + continue + } + if !contains(claim.AllowedValues, claimValue) { + logging.Log().Debugf("Claim value %s is not allowed by config %s.", logging.PrettyPrintObject(claimValue), logging.PrettyPrintObject(credentialConfig)) + allClaimsValid = false + break + } } } - + if allClaimsValid { + logging.Log().Debugf("No forbidden claim found for subject %s. Checked config was %s.", logging.PrettyPrintObject(subjectToVerify), logging.PrettyPrintObject(credentialConfig)) + return true + } } - logging.Log().Debugf("No forbidden claim found for subject %s. Checked config was %s.", logging.PrettyPrintObject(subjectToVerfiy), logging.PrettyPrintObject(credentialConfig)) - return true + logging.Log().Debugf("No credential config matched for subject %s. Config: %s.", logging.PrettyPrintObject(subjectToVerify), logging.PrettyPrintObject(credentialConfig)) + return false } -func verifyWithJsonPath(subjectToVerfiy verifiable.Subject, claim tir.Claim) (result bool) { - jsonSubject, _ := json.Marshal(subjectToVerfiy.CustomFields) +func verifyWithJsonPath(subjectToVerify verifiable.Subject, claim tir.Claim) (result bool) { + jsonSubject, _ := json.Marshal(subjectToVerify.CustomFields) var subjectAsMap map[string]interface{} if err := json.Unmarshal(jsonSubject, &subjectAsMap); err != nil { logging.Log().Warnf("Was not able to unmarshal the subject, set to invalid. Err: %v", err) @@ -194,7 +199,7 @@ func toSliceOfMaps(raw []interface{}) []map[string]interface{} { for _, item := range raw { m, ok := item.(map[string]interface{}) if !ok { - logging.Log().Warnf("Was not able to convert the allowed values, dont allow anything. V: %v", item) + logging.Log().Warnf("Was not able to convert the allowed values, don't allow anything. V: %v", item) return []map[string]interface{}{} } result = append(result, m) @@ -241,7 +246,7 @@ func contains(interfaces []interface{}, interfaceToCheck interface{}) bool { for _, i := range interfaces { jsonBytes, err := json.Marshal(i) if err != nil { - logging.Log().Warn("Not able to marshal one of the intefaces.") + logging.Log().Warn("Not able to marshal one of the interfaces.") continue } if slices.Compare(jsonBytes, jsonBytesToCheck) == 0 { diff --git a/verifier/trustedissuer_test.go b/verifier/trustedissuer_test.go index e0a8a49..d34284c 100644 --- a/verifier/trustedissuer_test.go +++ b/verifier/trustedissuer_test.go @@ -15,42 +15,42 @@ import ( func TestVerifyWithJsonPath(t *testing.T) { type test struct { - testName string - path string - allowedValues []interface{} - credentialToVerifiy verifiable.Credential - expectedResult bool + testName string + path string + allowedValues []interface{} + credentialToVerify verifiable.Credential + expectedResult bool } tests := []test{ - {testName: "When the string claim matches, it should be valid.", path: "$.test", allowedValues: []interface{}{"value"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": "value"}), expectedResult: true}, - {testName: "When the string claim is contained in the allowed values, it should be valid.", path: "$.test", allowedValues: []interface{}{"value", "otherValue"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": "value"}), expectedResult: true}, - {testName: "When the claim does not exist, it should be valid.", path: "$.test", allowedValues: []interface{}{"value"}, credentialToVerifiy: getTestCredential(map[string]interface{}{}), expectedResult: true}, - {testName: "When the string claim does not match, it should be invalid.", path: "$.test", allowedValues: []interface{}{"value"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": "otherValue"}), expectedResult: false}, - {testName: "When the claim contains another type, it should be invalid.", path: "$.test", allowedValues: []interface{}{"value"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": 1}), expectedResult: false}, - {testName: "When the int claim matches, it should be valid.", path: "$.test", allowedValues: []interface{}{1.0}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": 1}), expectedResult: true}, - {testName: "When the int claim is contained in the allowed values, it should be valid.", path: "$.test", allowedValues: []interface{}{1.0, 2.0}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": 1}), expectedResult: true}, - {testName: "When the int claim does not match, it should be invalid.", path: "$.test", allowedValues: []interface{}{1.0}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": 2}), expectedResult: false}, - {testName: "When the bool claim matches, it should be valid.", path: "$.test", allowedValues: []interface{}{true}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": true}), expectedResult: true}, - {testName: "When the bool claim does not match, it should be invalid.", path: "$.test", allowedValues: []interface{}{true}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": false}), expectedResult: false}, - {testName: "When the object claim matches, it should be valid.", path: "$.test", allowedValues: []interface{}{map[string]interface{}{"a": "b"}}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"a": "b"}}), expectedResult: true}, - {testName: "When the object claim is contained, it should be valid.", path: "$.test", allowedValues: []interface{}{map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "c"}}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"a": "b"}}), expectedResult: true}, - {testName: "When the object claim does not match, it should be invalid.", path: "$.test", allowedValues: []interface{}{map[string]interface{}{"a": "b"}}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"a": "c"}}), expectedResult: false}, - {testName: "When the string inside the claim matches, it should be valid.", path: "$.test.sub", allowedValues: []interface{}{"value"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"sub": "value"}}), expectedResult: true}, - {testName: "When the sub claim does not exist, it should be valid.", path: "$.test.sub", allowedValues: []interface{}{"value"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": "t"}), expectedResult: true}, - {testName: "When the string inside the claim matches, it should be valid.", path: "$.test.sub", allowedValues: []interface{}{"value"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"sub": "otherValue"}}), expectedResult: false}, - {testName: "When the string inside the claim does not match, it should be invalid.", path: "$.test.sub", allowedValues: []interface{}{"value"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"sub": map[string]interface{}{"sub": "value"}}}), expectedResult: false}, - {testName: "When the string inside the array matches, it should be valid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{"a"}}), expectedResult: true}, - {testName: "When the string array is contained, it should be valid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{}}), expectedResult: true}, - {testName: "When the string array is empty, it should be valid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{"a", "b"}}), expectedResult: true}, - {testName: "When the string array is equal, it should be valid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{"a", "b", "c"}}), expectedResult: true}, - {testName: "When the strings are contained that are not allowed, it should be invalid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{"a", "b", "c", "d"}}), expectedResult: false}, - {testName: "When the strings are contained that are not allowed, it should be invalid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{"d"}}), expectedResult: false}, - {testName: "When an array element is selected and the sub claim matches, it should be valid.", path: `$.test[?(@.a=="b")].role[*]`, allowedValues: []interface{}{"OPERATOR", "READER"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{map[string]interface{}{"a": "b", "role": []string{"OPERATOR"}}}}), expectedResult: true}, - {testName: "When the selected element is not contained, it should be valid.", path: `$.test[?(@.a=="b")].role[*]`, allowedValues: []interface{}{"OPERATOR", "READER"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{map[string]interface{}{"a": "c", "role": []string{"ADMIN"}}}}), expectedResult: true}, - {testName: "When the selected element does not have any such claims, it should be valid.", path: `$.test[?(@.a=="b")].role[*]`, allowedValues: []interface{}{"OPERATOR", "READER"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{map[string]interface{}{"a": "b"}}}), expectedResult: true}, - {testName: "When an array element is selected and the sub claim does not match, it should be invalid.", path: `$.test[?(@.a=="b")].role[*]`, allowedValues: []interface{}{"OPERATOR", "READER"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{map[string]interface{}{"a": "b", "role": []string{"ADMIN"}}}}), expectedResult: false}, - {testName: "When an array element is selected and the sub claim contains not allowed values, it should be invalid.", path: `$.test[?(@.a=="b")].role[*]`, allowedValues: []interface{}{"OPERATOR", "READER"}, credentialToVerifiy: getTestCredential(map[string]interface{}{"test": []interface{}{map[string]interface{}{"a": "b", "role": []string{"ADMIN", "OPERATOR", "READER"}}}}), expectedResult: false}, + {testName: "When the string claim matches, it should be valid.", path: "$.test", allowedValues: []interface{}{"value"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": "value"}), expectedResult: true}, + {testName: "When the string claim is contained in the allowed values, it should be valid.", path: "$.test", allowedValues: []interface{}{"value", "otherValue"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": "value"}), expectedResult: true}, + {testName: "When the claim does not exist, it should be valid.", path: "$.test", allowedValues: []interface{}{"value"}, credentialToVerify: getTestCredential(map[string]interface{}{}), expectedResult: true}, + {testName: "When the string claim does not match, it should be invalid.", path: "$.test", allowedValues: []interface{}{"value"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": "otherValue"}), expectedResult: false}, + {testName: "When the claim contains another type, it should be invalid.", path: "$.test", allowedValues: []interface{}{"value"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": 1}), expectedResult: false}, + {testName: "When the int claim matches, it should be valid.", path: "$.test", allowedValues: []interface{}{1.0}, credentialToVerify: getTestCredential(map[string]interface{}{"test": 1}), expectedResult: true}, + {testName: "When the int claim is contained in the allowed values, it should be valid.", path: "$.test", allowedValues: []interface{}{1.0, 2.0}, credentialToVerify: getTestCredential(map[string]interface{}{"test": 1}), expectedResult: true}, + {testName: "When the int claim does not match, it should be invalid.", path: "$.test", allowedValues: []interface{}{1.0}, credentialToVerify: getTestCredential(map[string]interface{}{"test": 2}), expectedResult: false}, + {testName: "When the bool claim matches, it should be valid.", path: "$.test", allowedValues: []interface{}{true}, credentialToVerify: getTestCredential(map[string]interface{}{"test": true}), expectedResult: true}, + {testName: "When the bool claim does not match, it should be invalid.", path: "$.test", allowedValues: []interface{}{true}, credentialToVerify: getTestCredential(map[string]interface{}{"test": false}), expectedResult: false}, + {testName: "When the object claim matches, it should be valid.", path: "$.test", allowedValues: []interface{}{map[string]interface{}{"a": "b"}}, credentialToVerify: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"a": "b"}}), expectedResult: true}, + {testName: "When the object claim is contained, it should be valid.", path: "$.test", allowedValues: []interface{}{map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "c"}}, credentialToVerify: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"a": "b"}}), expectedResult: true}, + {testName: "When the object claim does not match, it should be invalid.", path: "$.test", allowedValues: []interface{}{map[string]interface{}{"a": "b"}}, credentialToVerify: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"a": "c"}}), expectedResult: false}, + {testName: "When the string inside the claim matches, it should be valid.", path: "$.test.sub", allowedValues: []interface{}{"value"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"sub": "value"}}), expectedResult: true}, + {testName: "When the sub claim does not exist, it should be valid.", path: "$.test.sub", allowedValues: []interface{}{"value"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": "t"}), expectedResult: true}, + {testName: "When the string inside the claim matches, it should be valid.", path: "$.test.sub", allowedValues: []interface{}{"value"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"sub": "otherValue"}}), expectedResult: false}, + {testName: "When the string inside the claim does not match, it should be invalid.", path: "$.test.sub", allowedValues: []interface{}{"value"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": map[string]interface{}{"sub": map[string]interface{}{"sub": "value"}}}), expectedResult: false}, + {testName: "When the string inside the array matches, it should be valid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{"a"}}), expectedResult: true}, + {testName: "When the string array is contained, it should be valid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{}}), expectedResult: true}, + {testName: "When the string array is empty, it should be valid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{"a", "b"}}), expectedResult: true}, + {testName: "When the string array is equal, it should be valid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{"a", "b", "c"}}), expectedResult: true}, + {testName: "When the strings are contained that are not allowed, it should be invalid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{"a", "b", "c", "d"}}), expectedResult: false}, + {testName: "When the strings are contained that are not allowed, it should be invalid.", path: "$.test", allowedValues: []interface{}{"a", "b", "c"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{"d"}}), expectedResult: false}, + {testName: "When an array element is selected and the sub claim matches, it should be valid.", path: `$.test[?(@.a=="b")].role[*]`, allowedValues: []interface{}{"OPERATOR", "READER"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{map[string]interface{}{"a": "b", "role": []string{"OPERATOR"}}}}), expectedResult: true}, + {testName: "When the selected element is not contained, it should be valid.", path: `$.test[?(@.a=="b")].role[*]`, allowedValues: []interface{}{"OPERATOR", "READER"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{map[string]interface{}{"a": "c", "role": []string{"ADMIN"}}}}), expectedResult: true}, + {testName: "When the selected element does not have any such claims, it should be valid.", path: `$.test[?(@.a=="b")].role[*]`, allowedValues: []interface{}{"OPERATOR", "READER"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{map[string]interface{}{"a": "b"}}}), expectedResult: true}, + {testName: "When an array element is selected and the sub claim does not match, it should be invalid.", path: `$.test[?(@.a=="b")].role[*]`, allowedValues: []interface{}{"OPERATOR", "READER"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{map[string]interface{}{"a": "b", "role": []string{"ADMIN"}}}}), expectedResult: false}, + {testName: "When an array element is selected and the sub claim contains not allowed values, it should be invalid.", path: `$.test[?(@.a=="b")].role[*]`, allowedValues: []interface{}{"OPERATOR", "READER"}, credentialToVerify: getTestCredential(map[string]interface{}{"test": []interface{}{map[string]interface{}{"a": "b", "role": []string{"ADMIN", "OPERATOR", "READER"}}}}), expectedResult: false}, } for _, tc := range tests { @@ -58,7 +58,7 @@ func TestVerifyWithJsonPath(t *testing.T) { logging.Log().Info("TestVerifyWithJsonPath +++++++++++++++++ Running test: ", tc.testName) - result := verifyWithJsonPath(tc.credentialToVerifiy.Contents().Subject[0], tir.Claim{Path: tc.path, AllowedValues: tc.allowedValues}) + result := verifyWithJsonPath(tc.credentialToVerify.Contents().Subject[0], tir.Claim{Path: tc.path, AllowedValues: tc.allowedValues}) if result != tc.expectedResult { t.Errorf("%s - Expected result %v but was %v.", tc.testName, tc.expectedResult, result) return @@ -71,7 +71,7 @@ func TestVerifyVC_Issuers(t *testing.T) { type test struct { testName string - credentialToVerifiy verifiable.Credential + credentialToVerify verifiable.Credential verificationContext ValidationContext participantsList []string tirResponse tir.TrustedIssuer @@ -81,60 +81,60 @@ func TestVerifyVC_Issuers(t *testing.T) { tests := []test{ {testName: "If no trusted issuer is configured in the list, the vc should be rejected.", - credentialToVerifiy: getVerifiableCredential("test", "claim"), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("test", "claim"), verificationContext: getVerificationContext(), participantsList: []string{}, tirResponse: tir.TrustedIssuer{}, tirError: nil, expectedResult: false}, {testName: "If the trusted issuer is invalid, the vc should be rejected.", - credentialToVerifiy: getVerifiableCredential("test", "claim"), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("test", "claim"), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: tir.TrustedIssuer{Attributes: []tir.IssuerAttribute{{Body: "invalidBody"}}}, tirError: nil, expectedResult: false}, {testName: "If the type is not included, the vc should be rejected.", - credentialToVerifiy: getTypedCredential("AnotherType", "testClaim", "testValue"), verificationContext: getVerificationContext(), + credentialToVerify: getTypedCredential("AnotherType", "testClaim", "testValue"), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "AnotherType", map[string][]interface{}{})}), tirError: nil, expectedResult: false}, {testName: "If one of the types is not allowed, the vc should be rejected.", - credentialToVerifiy: getMultiTypeCredential([]string{"VerifiableCredential", "SecondType"}, "testClaim", "testValue"), verificationContext: getVerificationContext(), + credentialToVerify: getMultiTypeCredential([]string{"VerifiableCredential", "SecondType"}, "testClaim", "testValue"), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{})}), tirError: nil, expectedResult: false}, {testName: "If no restriction is configured, the vc should be accepted.", - credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{})}), tirError: nil, expectedResult: true}, {testName: "If no restricted claim is included, the vc should be accepted.", - credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"another": {"claim"}})}), tirError: nil, expectedResult: true}, {testName: "If the (string)claim is allowed, the vc should be accepted.", - credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {"testValue"}})}), tirError: nil, expectedResult: true}, {testName: "If the (string)claim is one of the allowed values, the vc should be accepted.", - credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {"testValue", "anotherAllowedValue"}})}), tirError: nil, expectedResult: true}, {testName: "If the (string)claim is not allowed, the vc should be rejected.", - credentialToVerifiy: getVerifiableCredential("testClaim", "anotherValue"), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", "anotherValue"), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {"testValue"}})}), tirError: nil, expectedResult: false}, {testName: "If the (number)claim is allowed, the vc should be accepted.", - credentialToVerifiy: getVerifiableCredential("testClaim", 1), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", 1), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {1}})}), tirError: nil, expectedResult: true}, {testName: "If the (number)claim is not allowed, the vc should be rejected.", - credentialToVerifiy: getVerifiableCredential("testClaim", 2), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", 2), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {1}})}), tirError: nil, expectedResult: false}, {testName: "If the (object)claim is allowed, the vc should be accepted.", - credentialToVerifiy: getVerifiableCredential("testClaim", map[string]interface{}{"some": "object"}), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", map[string]interface{}{"some": "object"}), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {map[string]interface{}{"some": "object"}}})}), tirError: nil, expectedResult: true}, {testName: "If the all claim allowed, the vc should be allowed.", - credentialToVerifiy: getMultiClaimCredential(map[string]interface{}{"claimA": map[string]interface{}{"some": "object"}, "claimB": "b"}), verificationContext: getVerificationContext(), + credentialToVerify: getMultiClaimCredential(map[string]interface{}{"claimA": map[string]interface{}{"some": "object"}, "claimB": "b"}), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"claimA": {map[string]interface{}{"some": "object"}}, "claimB": {"b"}})}), tirError: nil, expectedResult: true}, {testName: "If a wildcard til is configured for the type, the vc should be allowed.", - credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getWildcardVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", "testValue"), verificationContext: getWildcardVerificationContext(), participantsList: []string{"did:test:issuer"}, tirError: nil, expectedResult: true}, {testName: "If all types are allowed, the vc should be allowed.", - credentialToVerifiy: getMultiTypeCredential([]string{"VerifiableCredential", "SecondType"}, "testClaim", "testValue"), verificationContext: getWildcardAndNormalVerificationContext(), + credentialToVerify: getMultiTypeCredential([]string{"VerifiableCredential", "SecondType"}, "testClaim", "testValue"), verificationContext: getWildcardAndNormalVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "SecondType", map[string][]interface{}{}), getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{})}), tirError: nil, expectedResult: true}, {testName: "If not all claims are allowed, the vc should be rejected.", - credentialToVerifiy: getMultiClaimCredential(map[string]interface{}{"claimA": map[string]interface{}{"some": "object"}, "claimB": "b"}), verificationContext: getVerificationContext(), + credentialToVerify: getMultiClaimCredential(map[string]interface{}{"claimA": map[string]interface{}{"some": "object"}, "claimB": "b"}), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"claimA": {map[string]interface{}{"some": "object"}}, "claimB": {"c"}})}), tirError: nil, expectedResult: false}, {testName: "If the trusted-issuers-registry responds with an error, the vc should be rejected.", - credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), participantsList: []string{"did:test:issuer"}, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{})}), tirError: errors.New("some-error"), expectedResult: false}, {testName: "If an invalid verification context is provided, the credential should be rejected.", - credentialToVerifiy: getVerifiableCredential("test", "claim"), verificationContext: "No-context", participantsList: []string{}, tirResponse: tir.TrustedIssuer{}, tirError: nil, expectedResult: false}, + credentialToVerify: getVerifiableCredential("test", "claim"), verificationContext: "No-context", participantsList: []string{}, tirResponse: tir.TrustedIssuer{}, tirError: nil, expectedResult: false}, {testName: "If a wildcard til and another til is configured for the type, the vc should be rejected.", - credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getInvalidMixedVerificationContext(), + credentialToVerify: getVerifiableCredential("testClaim", "testValue"), verificationContext: getInvalidMixedVerificationContext(), participantsList: []string{"did:test:issuer"}, tirError: nil, expectedResult: false}, } @@ -143,8 +143,86 @@ func TestVerifyVC_Issuers(t *testing.T) { logging.Log().Info("TestVerifyVC +++++++++++++++++ Running test: ", tc.testName) - trustedIssuerVerficationService := TrustedIssuerValidationService{mockTirClient{tc.participantsList, tc.tirResponse, tc.tirError}} - result, _ := trustedIssuerVerficationService.ValidateVC(&tc.credentialToVerifiy, tc.verificationContext) + trustedIssuerVerificationService := TrustedIssuerValidationService{mockTirClient{tc.participantsList, tc.tirResponse, tc.tirError}} + result, _ := trustedIssuerVerificationService.ValidateVC(&tc.credentialToVerify, tc.verificationContext) + if result != tc.expectedResult { + t.Errorf("%s - Expected result %v but was %v.", tc.testName, tc.expectedResult, result) + return + } + }) + } +} + +func TestVerifyForType(t *testing.T) { + + type test struct { + testName string + subject verifiable.Subject + credentialConfig []tir.Credential + expectedResult bool + } + + tests := []test{ + {testName: "When there are no credential configs, it should be invalid.", + subject: verifiable.Subject{CustomFields: map[string]any{"role": "ADMIN"}}, + credentialConfig: []tir.Credential{}, + expectedResult: false}, + {testName: "When the config has no claims, it should be valid.", + subject: verifiable.Subject{CustomFields: map[string]any{"role": "ADMIN"}}, + credentialConfig: []tir.Credential{{CredentialsType: "VerifiableCredential", Claims: []tir.Claim{}}}, + expectedResult: true}, + {testName: "When the single config has all claims matching, it should be valid.", + subject: verifiable.Subject{CustomFields: map[string]any{"role": "ADMIN"}}, + credentialConfig: []tir.Credential{{CredentialsType: "VerifiableCredential", Claims: []tir.Claim{{Name: "role", AllowedValues: []any{"ADMIN"}}}}}, + expectedResult: true}, + {testName: "When there are multiple configs and the first is invalid but the second matches, it should be valid (OR logic).", + subject: verifiable.Subject{CustomFields: map[string]any{"role": "READER"}}, + credentialConfig: []tir.Credential{ + {CredentialsType: "VerifiableCredential", Claims: []tir.Claim{{Name: "role", AllowedValues: []any{"ADMIN"}}}}, + {CredentialsType: "VerifiableCredential", Claims: []tir.Claim{{Name: "role", AllowedValues: []any{"READER"}}}}, + }, + expectedResult: true}, + {testName: "When there are multiple configs and the first matches, it should be valid without checking the rest (OR short-circuit).", + subject: verifiable.Subject{CustomFields: map[string]any{"role": "ADMIN"}}, + credentialConfig: []tir.Credential{ + {CredentialsType: "VerifiableCredential", Claims: []tir.Claim{{Name: "role", AllowedValues: []any{"ADMIN"}}}}, + {CredentialsType: "VerifiableCredential", Claims: []tir.Claim{{Name: "role", AllowedValues: []any{"READER"}}}}, + }, + expectedResult: true}, + {testName: "When there are multiple configs and one of them has all claims matching, it should be valid.", + subject: verifiable.Subject{CustomFields: map[string]any{"role": "ADMIN", "extra": "value"}}, + credentialConfig: []tir.Credential{ + {CredentialsType: "VerifiableCredential", Claims: []tir.Claim{ + {Name: "role", AllowedValues: []any{"ADMIN"}}, + {Name: "extra", AllowedValues: []any{"other"}}, + }}, + {CredentialsType: "VerifiableCredential", Claims: []tir.Claim{ + {Name: "role", AllowedValues: []any{"ADMIN"}}, + {Name: "extra", AllowedValues: []any{"value"}}, + }}, + }, + expectedResult: true}, + {testName: "When there are multiple configs and a claim in the middle of one fails, the next config should still be evaluated.", + subject: verifiable.Subject{CustomFields: map[string]any{"role": "OPERATOR", "level": "2"}}, + credentialConfig: []tir.Credential{ + {CredentialsType: "VerifiableCredential", Claims: []tir.Claim{ + {Name: "role", AllowedValues: []any{"ADMIN"}}, + {Name: "level", AllowedValues: []any{"1"}}, + }}, + {CredentialsType: "VerifiableCredential", Claims: []tir.Claim{ + {Name: "role", AllowedValues: []any{"OPERATOR"}}, + {Name: "level", AllowedValues: []any{"2"}}, + }}, + }, + expectedResult: true}, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + + logging.Log().Info("TestVerifyForType +++++++++++++++++ Running test: ", tc.testName) + + result := verifyForType(tc.subject, tc.credentialConfig) if result != tc.expectedResult { t.Errorf("%s - Expected result %v but was %v.", tc.testName, tc.expectedResult, result) return