Skip to content

Commit

Permalink
better validation and testing
Browse files Browse the repository at this point in the history
  • Loading branch information
woutslakhorst committed Dec 18, 2024
1 parent da23114 commit 6ca135e
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 31 deletions.
8 changes: 6 additions & 2 deletions vcr/credential/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,17 @@ func TestFindValidator(t *testing.T) {
assert.IsType(t, defaultCredentialValidator{}, FindValidator(vc.VerifiableCredential{}))
})

t.Run("validator and builder found for NutsOrganizationCredential", func(t *testing.T) {
t.Run("validator found for NutsOrganizationCredential", func(t *testing.T) {
assert.IsType(t, nutsOrganizationCredentialValidator{}, FindValidator(test.ValidNutsOrganizationCredential(t)))
})

t.Run("validator and builder found for NutsAuthorizationCredential", func(t *testing.T) {
t.Run("validator found for NutsAuthorizationCredential", func(t *testing.T) {
assert.IsType(t, nutsAuthorizationCredentialValidator{}, FindValidator(test.ValidNutsAuthorizationCredential(t)))
})

t.Run("validator found for X509Credential", func(t *testing.T) {
assert.IsType(t, x509CredentialValidator{}, FindValidator(test.ValidX509Credential(t)))
})
}

func TestExtractTypes(t *testing.T) {
Expand Down
56 changes: 41 additions & 15 deletions vcr/credential/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"fmt"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/vdr/didx509"
"net/url"
"strings"

"github.com/nuts-foundation/go-did/did"
Expand Down Expand Up @@ -288,29 +289,54 @@ func (d x509CredentialValidator) Validate(credential vc.VerifiableCredential) er

// validatePolicyAssertions checks if the credentialSubject claims match the did issuer policies
func validatePolicyAssertions(credential vc.VerifiableCredential) error {
// add a : to the end of the string so we can always add an end character for string matching
// this eliminates the possibility to use substrings as assertions.
policyString := credential.Issuer.String() + ":"

// get base form of all credentialSubject
var target = make([]map[string]interface{}, 0)
if err := credential.UnmarshalCredentialSubject(&target); err != nil {
return err
}
if len(target) != 1 {
return errors.New("single CredentialSubject expected")

// we create a map of policyName to policyValue, then we split the policyValue into another map
policyMap := make(map[string]map[string]string)
policies := strings.Split(credential.Issuer.String(), "::")
if len(policies) < 2 {
return fmt.Errorf("invalid did:x509 policy")
}
for _, policy := range policies[1:] {
policySplit := strings.Split(policy, ":")
if len(policySplit) < 2 {
return fmt.Errorf("invalid did:x509 policy '%s'", policy)
}
policyName := policySplit[0]
policyMap[policyName] = make(map[string]string)
for i := 1; i < len(policySplit); i += 2 {
unscaped, _ := url.PathUnescape(policySplit[i+1])
policyMap[policyName][policySplit[i]] = unscaped
}
}
credentialSubject := target[0]

// remove id from target
delete(credentialSubject, "id")
// we usually don't use multiple credentialSubjects, but for this validation it doesn't matter
for _, credentialSubject := range target {
// remove id from target
delete(credentialSubject, "id")

// for each assertion create a string as "%s:%s" with key/value
// check if the resulting string is present in the policyString
for key, value := range credentialSubject {
assertionString := fmt.Sprintf("%s:%s:", key, value)
if !strings.Contains(policyString, assertionString) {
return fmt.Errorf("assertion '%s' not found in issuer policy", assertionString)
// for each assertion create a string as "%s:%s" with key/value
// check if the resulting string is present in the policyString
for key, value := range credentialSubject {
split := strings.Split(key, ":")
if len(split) != 2 {
return fmt.Errorf("invalid credentialSubject assertion name '%s'", key)
}
policyValueMap, ok := policyMap[split[0]]
if !ok {
return fmt.Errorf("policy '%s' not found in did:x509 policy", split[0])
}
policyValue, ok := policyValueMap[split[1]]
if !ok {
return fmt.Errorf("assertion '%s' not found in did:x509 policy", key)
}
if value != policyValue {
return fmt.Errorf("invalid assertion value '%s' for '%s' did:x509 policy", value, key)
}
}
}

Expand Down
57 changes: 45 additions & 12 deletions vcr/credential/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
package credential

import (
"testing"
"time"

"github.com/lestrrat-go/jwx/v2/jwt"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
Expand All @@ -30,8 +33,6 @@ import (
"github.com/nuts-foundation/nuts-node/vdr"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"testing"
"time"
)

func init() {
Expand Down Expand Up @@ -514,19 +515,51 @@ func TestX509CredentialValidator_Validate(t *testing.T) {
assert.ErrorIs(t, err, errValidation)
assert.ErrorIs(t, err, did.ErrInvalidDID)
})
t.Run("invalid assertion value", func(t *testing.T) {
x509credential := test.ValidX509Credential(t, func(builder *jwt.Builder) *jwt.Builder {
builder.Claim("vc", map[string]interface{}{
"credentialSubject": map[string]interface{}{

t.Run("failed validation", func(t *testing.T) {

testCases := []struct {
name string
claim map[string]interface{}
expectedError string
}{
{
name: "invalid assertion value",
claim: map[string]interface{}{
"san:otherName": "A_BIG_STRIN",
},
})
return builder
})
expectedError: "invalid assertion value 'A_BIG_STRIN' for 'san:otherName' did:x509 policy",
},
{
name: "unknown assertion",
claim: map[string]interface{}{
"san:ip": "10.0.0.1",
},
expectedError: "assertion 'san:ip' not found in did:x509 policy",
},
{
name: "unknown policy",
claim: map[string]interface{}{
"stan:ip": "10.0.0.1",
},
expectedError: "policy 'stan' not found in did:x509 policy",
},
}

err := validator.Validate(x509credential)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
x509credential := test.ValidX509Credential(t, func(builder *jwt.Builder) *jwt.Builder {
builder.Claim("vc", map[string]interface{}{
"credentialSubject": tc.claim,
})
return builder
})

assert.ErrorIs(t, err, errValidation)
assert.ErrorContains(t, err, "assertion 'san:otherName:A_BIG_STRIN:' not found in issuer policy")
err := validator.Validate(x509credential)

assert.ErrorIs(t, err, errValidation)
assert.ErrorContains(t, err, tc.expectedError)
})
}
})
}
8 changes: 6 additions & 2 deletions vcr/test/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func ValidX509Credential(t *testing.T, options ...credentialOption) vc.Verifiabl
rootCertificate := certs[len(certs)-1]
rootKey := keys[len(keys)-1]
rootHash := sha256.Sum256(rootCertificate.Raw)
rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::san:otherName:%s", "sha256", base64.RawURLEncoding.EncodeToString(rootHash[:]), otherNameValue))
rootDID := did.MustParseDID(fmt.Sprintf("did:x509:0:%s:%s::subject:C:NL:O:NUTS%%20Foundation:L:Amsterdam:CN:www.example.com::san:otherName:%s", "sha256", base64.RawURLEncoding.EncodeToString(rootHash[:]), otherNameValue))
x5c := cert.Chain{}
for _, cert := range certs {
_ = x5c.AddString(base64.StdEncoding.EncodeToString(cert.Raw))
Expand All @@ -160,9 +160,13 @@ func ValidX509Credential(t *testing.T, options ...credentialOption) vc.Verifiabl
Subject("did:example:1").
Claim("vc", map[string]interface{}{
"@context": []string{"https://www.w3.org/2018/credentials/v1"},
"type": []string{"VerifiableCredential"},
"type": []string{vc.VerifiableCredentialType, "X509Credential"},
"credentialSubject": map[string]interface{}{
"id": rootDID.String(),
"subject:C": "NL",
"subject:O": "NUTS Foundation",
"subject:L": "Amsterdam",
"subject:CN": "www.example.com",
"san:otherName": otherNameValue,
},
})
Expand Down

0 comments on commit 6ca135e

Please sign in to comment.