Skip to content

Commit

Permalink
Merge pull request hyperledger-archives#997 from kdimak/issue-992
Browse files Browse the repository at this point in the history
feat: VC context options
  • Loading branch information
troyronda authored Dec 20, 2019
2 parents 98b4760 + 6e47aa4 commit ae79892
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 8 deletions.
127 changes: 122 additions & 5 deletions pkg/doc/verifiable/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,35 @@ const defaultSchema = `{
}
`

// https://www.w3.org/TR/vc-data-model/#data-schemas
const jsonSchema2018Type = "JsonSchemaValidator2018"

const vpType = "VerifiablePresentation"
const (
// https://www.w3.org/TR/vc-data-model/#base-context
baseContext = "https://www.w3.org/2018/credentials/v1"

// https://www.w3.org/TR/vc-data-model/#types
vcType = "VerifiableCredential"

// https://www.w3.org/TR/vc-data-model/#presentations-0
vpType = "VerifiablePresentation"
)

// vcModelValidationMode defines constraint put on context and type of VC.
type vcModelValidationMode int

const (
// baseContextValidation when defined it's validated that only the fields and values (when applicable)
// are present in the document. No extra fields are allowed (outside of credentialSubject).
baseContextValidation vcModelValidationMode = iota

// baseContextExtendedValidation when set it's validated that fields that are specified in base context are
// as specified. Additional fields are allowed.
baseContextExtendedValidation

// jsonldValidation Use the JSON LD parser for validation.
jsonldValidation
)

// SchemaCache defines a cache of credential schemas.
type SchemaCache interface {
Expand Down Expand Up @@ -397,6 +423,9 @@ type credentialOpts struct {
issuerPublicKeyFetcher PublicKeyFetcher
disabledCustomSchema bool
schemaLoader *CredentialSchemaLoader
modelValidationMode vcModelValidationMode
allowedCustomContexts map[string]bool
allowedCustomTypes map[string]bool
}

// CredentialOpt is the Verifiable Credential decoding option
Expand Down Expand Up @@ -426,6 +455,41 @@ func WithCredentialSchemaLoader(loader *CredentialSchemaLoader) CredentialOpt {
}
}

// WithJSONLDValidation uses the JSON LD parser for validation.
func WithJSONLDValidation() CredentialOpt {
return func(opts *credentialOpts) {
opts.modelValidationMode = jsonldValidation
}
}

// WithBaseContextValidation validates that only the fields and values (when applicable) are present
// in the document. No extra fields are allowed (outside of credentialSubject).
func WithBaseContextValidation() CredentialOpt {
return func(opts *credentialOpts) {
opts.modelValidationMode = baseContextValidation
}
}

// WithBaseContextExtendedValidation validates that fields that are specified in base context are as specified.
// Additional fields are allowed
func WithBaseContextExtendedValidation(customContexts, customTypes []string) CredentialOpt {
return func(opts *credentialOpts) {
opts.modelValidationMode = baseContextExtendedValidation

opts.allowedCustomContexts = make(map[string]bool)
for _, context := range customContexts {
opts.allowedCustomContexts[context] = true
}
opts.allowedCustomContexts[baseContext] = true

opts.allowedCustomTypes = make(map[string]bool)
for _, context := range customTypes {
opts.allowedCustomTypes[context] = true
}
opts.allowedCustomTypes[vcType] = true
}
}

// decodeIssuer decodes raw issuer.
//
// Issuer can be defined by:
Expand Down Expand Up @@ -506,10 +570,10 @@ func decodeCredentialSchema(data []byte) ([]TypedID, error) {
// by checking CustomFields of Credential and/or unmarshalling the JSON to custom date structure.
func NewCredential(vcData []byte, opts ...CredentialOpt) (*Credential, []byte, error) {
// Apply options.
crOpts := parseCredentialOpts(opts)
vcOpts := parseCredentialOpts(opts)

// Decode credential (e.g. from JWT).
vcDataDecoded, err := decodeRaw(vcData, crOpts.issuerPublicKeyFetcher)
vcDataDecoded, err := decodeRaw(vcData, vcOpts.issuerPublicKeyFetcher)
if err != nil {
return nil, nil, fmt.Errorf("decode new credential: %w", err)
}
Expand All @@ -534,7 +598,7 @@ func NewCredential(vcData []byte, opts ...CredentialOpt) (*Credential, []byte, e
}

// Validate raw credential.
err = validate(vcDataDecoded, schemas, crOpts)
err = validate(vcDataDecoded, schemas, vcOpts)
if err != nil {
return nil, nil, fmt.Errorf("validate new credential: %w", err)
}
Expand All @@ -545,9 +609,60 @@ func NewCredential(vcData []byte, opts ...CredentialOpt) (*Credential, []byte, e
return nil, nil, fmt.Errorf("build new credential: %w", err)
}

err = postValidateCredential(vc, vcOpts)
if err != nil {
return nil, nil, err
}

return vc, vcDataDecoded, nil
}

func postValidateCredential(vc *Credential, vcOpts *credentialOpts) error {
// Credential and type constraint.
switch vcOpts.modelValidationMode {
case jsonldValidation:
// todo support JSON-LD validation (https://github.com/hyperledger/aries-framework-go/issues/952)
return nil

case baseContextValidation:
return validateBaseOnlyContextType(vc)

case baseContextExtendedValidation:
return validateCustomContextType(vc, vcOpts)

default:
return fmt.Errorf("unsupported vcModelValidationMode: %v", vcOpts.modelValidationMode)
}
}

func validateBaseOnlyContextType(vc *Credential) error {
if len(vc.Types) > 1 || vc.Types[0] != vcType {
return errors.New("violated type constraint: not base only type defined")
}

if len(vc.Context) > 1 || vc.Context[0] != baseContext {
return errors.New("violated @context constraint: not base only @context defined")
}

return nil
}

func validateCustomContextType(vc *Credential, vcOpts *credentialOpts) error {
for _, vcContext := range vc.Context {
if _, ok := vcOpts.allowedCustomContexts[vcContext]; !ok {
return fmt.Errorf("not allowed @context: %s", vcContext)
}
}

for _, vcType := range vc.Types {
if _, ok := vcOpts.allowedCustomTypes[vcType]; !ok {
return fmt.Errorf("not allowed type: %s", vcType)
}
}

return nil
}

// CustomCredentialProducer is a factory for Credentials with extended data model.
type CustomCredentialProducer interface {
// Accept checks if producer is capable of building extended Credential data model.
Expand Down Expand Up @@ -656,7 +771,9 @@ func loadCredentialSchemas(vcBytes []byte) ([]TypedID, error) {
}

func parseCredentialOpts(opts []CredentialOpt) *credentialOpts {
crOpts := &credentialOpts{}
crOpts := &credentialOpts{
modelValidationMode: jsonldValidation,
}

for _, opt := range opts {
opt(crOpts)
Expand Down
141 changes: 141 additions & 0 deletions pkg/doc/verifiable/credential_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,50 @@ func TestWithCredentialSchemaLoader(t *testing.T) {
require.Nil(t, opts.schemaLoader.cache)
}

func TestWithAnyContextAndType(t *testing.T) {
credentialOpt := WithJSONLDValidation()
require.NotNil(t, credentialOpt)

opts := &credentialOpts{}
credentialOpt(opts)
require.Equal(t, jsonldValidation, opts.modelValidationMode)
require.Empty(t, opts.allowedCustomContexts)
require.Empty(t, opts.allowedCustomTypes)
}

func TestWithBaseOnlyContextAndType(t *testing.T) {
credentialOpt := WithBaseContextValidation()
require.NotNil(t, credentialOpt)

opts := &credentialOpts{}
credentialOpt(opts)
require.Equal(t, baseContextValidation, opts.modelValidationMode)
require.Empty(t, opts.allowedCustomContexts)
require.Empty(t, opts.allowedCustomTypes)
}

func TestWithCustomContextAndType(t *testing.T) {
credentialOpt := WithBaseContextExtendedValidation(
[]string{"https://www.w3.org/2018/credentials/examples/v1"},
[]string{"UniversityDegreeCredential", "AlumniCredential"})
require.NotNil(t, credentialOpt)

opts := &credentialOpts{}
credentialOpt(opts)
require.Equal(t, baseContextExtendedValidation, opts.modelValidationMode)

require.Equal(t, map[string]bool{
"https://www.w3.org/2018/credentials/v1": true,
"https://www.w3.org/2018/credentials/examples/v1": true},
opts.allowedCustomContexts)

require.Equal(t, map[string]bool{
"VerifiableCredential": true,
"UniversityDegreeCredential": true,
"AlumniCredential": true},
opts.allowedCustomTypes)
}

func TestCustomCredentialJsonSchemaValidator2018(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
rawMap := make(map[string]interface{})
Expand Down Expand Up @@ -1135,3 +1179,100 @@ func TestCredential_CreatePresentation(t *testing.T) {
require.Equal(t, []string{"VerifiablePresentation"}, vp.Type)
require.Equal(t, vc.Context, vp.Context)
}

func TestCredential_postValidateCredential(t *testing.T) {
r := require.New(t)

t.Run("test AnyContextAndType constraint", func(t *testing.T) {
r.NoError(postValidateCredential(&Credential{},
&credentialOpts{modelValidationMode: jsonldValidation}))
})

t.Run("test BaseOnlyContextAndType constraint", func(t *testing.T) {
r.NoError(postValidateCredential(
&Credential{
Types: []string{"VerifiableCredential"},
Context: []string{"https://www.w3.org/2018/credentials/v1"}},
&credentialOpts{modelValidationMode: baseContextValidation}))

err := postValidateCredential(
&Credential{
Types: []string{"VerifiableCredential", "UniversityDegreeCredential"},
Context: []string{"https://www.w3.org/2018/credentials/v1"}},
&credentialOpts{modelValidationMode: baseContextValidation})
r.Error(err)
r.EqualError(err, "violated type constraint: not base only type defined")

err = postValidateCredential(
&Credential{
Types: []string{"UniversityDegreeCredential"},
Context: []string{"https://www.w3.org/2018/credentials/v1"}},
&credentialOpts{modelValidationMode: baseContextValidation})
r.Error(err)
r.EqualError(err, "violated type constraint: not base only type defined")

err = postValidateCredential(
&Credential{
Types: []string{"VerifiableCredential"},
Context: []string{"https://www.w3.org/2018/credentials/v1", "https://www.exaple.org/udc/v1"}},
&credentialOpts{modelValidationMode: baseContextValidation})
r.Error(err)
r.EqualError(err, "violated @context constraint: not base only @context defined")

err = postValidateCredential(
&Credential{
Types: []string{"VerifiableCredential"},
Context: []string{"https://www.exaple.org/udc/v1"}},
&credentialOpts{modelValidationMode: baseContextValidation})
r.Error(err)
r.EqualError(err, "violated @context constraint: not base only @context defined")
})

t.Run("test CustomContextAndType constraint", func(t *testing.T) {
r.NoError(postValidateCredential(
&Credential{
Types: []string{"VerifiableCredential", "AlumniCredential"},
Context: []string{"https://www.w3.org/2018/credentials/v1", "https://www.exaple.org/alumni/v1"}},
&credentialOpts{
modelValidationMode: baseContextExtendedValidation,
allowedCustomTypes: map[string]bool{
"VerifiableCredential": true,
"AlumniCredential": true},
allowedCustomContexts: map[string]bool{
"https://www.w3.org/2018/credentials/v1": true,
"https://www.exaple.org/alumni/v1": true},
}))

err := postValidateCredential(
&Credential{
Types: []string{"VerifiableCredential", "UniversityDegreeCredential"},
Context: []string{"https://www.w3.org/2018/credentials/v1", "https://www.exaple.org/alumni/v1"}},
&credentialOpts{
modelValidationMode: baseContextExtendedValidation,
allowedCustomTypes: map[string]bool{
"VerifiableCredential": true,
"AlumniCredential": true},
allowedCustomContexts: map[string]bool{
"https://www.w3.org/2018/credentials/v1": true,
"https://www.exaple.org/alumni/v1": true},
})
r.Error(err)
r.EqualError(err, "not allowed type: UniversityDegreeCredential")

err = postValidateCredential(
&Credential{
Types: []string{"VerifiableCredential", "AlumniCredential"},
Context: []string{"https://www.w3.org/2018/credentials/v1", "https://www.exaple.org/udc/v1"}},
&credentialOpts{
modelValidationMode: baseContextExtendedValidation,
allowedCustomTypes: map[string]bool{
"VerifiableCredential": true,
"AlumniCredential": true},
allowedCustomContexts: map[string]bool{
"https://www.w3.org/2018/credentials/v1": true,
"https://www.exaple.org/alumni/v1": true},
})
r.Error(err)
r.EqualError(err, "not allowed @context: https://www.exaple.org/udc/v1")
})
}
15 changes: 12 additions & 3 deletions pkg/doc/verifiable/test-suite/verifiable_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"

"github.com/square/go-jose/v3"

Expand Down Expand Up @@ -46,7 +47,7 @@ func main() {
if *isPresentation {
encodeVPToJSON(vcBytes)
} else {
encodeVCToJSON(vcBytes)
encodeVCToJSON(vcBytes, filepath.Base(inputFile))
}

return
Expand Down Expand Up @@ -187,8 +188,16 @@ func parseKeys(packedKeys string) (private, public interface{}) {
return privateKey, publicKey
}

func encodeVCToJSON(vcBytes []byte) {
credential, _, err := verifiable.NewCredential(vcBytes, verifiable.WithNoCustomSchemaCheck())
func encodeVCToJSON(vcBytes []byte, testFileName string) {
vcOpts := []verifiable.CredentialOpt{verifiable.WithNoCustomSchemaCheck()}

// This are special test cases which should be made more precise in VC Test Suite.
// See https://github.com/w3c/vc-test-suite/issues/96 for more information.
if testFileName == "example-1-bad-cardinality.jsonld" || testFileName == "example-3-bad-cardinality.jsonld" {
vcOpts = append(vcOpts, verifiable.WithBaseContextValidation())
}

credential, _, err := verifiable.NewCredential(vcBytes, vcOpts...)
if err != nil {
abort("failed to decode credential: %v", err)
}
Expand Down
9 changes: 9 additions & 0 deletions scripts/run_vc_test_suite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,12 @@ cat ${BUILD_DIR}/${VC_TEST_SUITE}/summary.json
echo "See full test suite results at ${SUITE_DIR}/implementations/${REPORT_NAME}-report.json"

cd $DIR

echo
if grep -q "\"failures\": 0" ${BUILD_DIR}/${VC_TEST_SUITE}/summary.json; then
echo "Verifiable Credential test suite passed!"
exit 0
else
echo "Verifiable Credential test suite did not pass"
exit 1
fi

0 comments on commit ae79892

Please sign in to comment.