From 958a02c60c7abf6d00051e98a357cfefe9ec7992 Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Wed, 19 Apr 2023 14:44:17 +0200 Subject: [PATCH 01/12] Config structure --- config/config.go | 11 +++++++++++ config/data/config_test.yaml | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 8bc59ee9..655de198 100644 --- a/config/config.go +++ b/config/config.go @@ -48,4 +48,15 @@ type Verifier struct { SessionExpiry int `mapstructure:"sessionExpiry" default:"30"` // scope to be used in the authentication request RequestScope string `mapstructure:"requestScope"` + + PolicyConfig Policies `mapstructure:"policies"` +} + +type Policies struct { + DefaultPolicies PolicyMap `mapstructure:"default"` + CertificateTypeSpecificPolicies map[string]PolicyMap `mapstructure:"certificateTypeSpecific"` } + +type PolicyMap map[string]PolicyConfigParameters + +type PolicyConfigParameters map[string]interface{} diff --git a/config/data/config_test.yaml b/config/data/config_test.yaml index ef279914..62be0776 100644 --- a/config/data/config_test.yaml +++ b/config/data/config_test.yaml @@ -13,6 +13,13 @@ verifier: did: "did:key:somekey" tirAddress: "https://test.dev/trusted_issuer/v3/issuers/" sessionExpiry: 30 - + policies: + default: + SignaturePolicy: {} + TrustedIssuerRegistryPolicy: + registryAddress: "waltId.com" + certificateTypeSpecific: + "gx:compliance": + ValidFromBeforePolicy: {} ssiKit: auditorURL: http://waltid:7003 \ No newline at end of file From 46c776236a76681ec5881c70e1242199035d1326 Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Thu, 20 Apr 2023 11:28:54 +0200 Subject: [PATCH 02/12] extract VC, add external verifier --- config/config.go | 8 ++-- config/data/config_test.yaml | 2 +- config/provider_test.go | 13 +++++ go.mod | 1 + go.sum | 1 + ssikit/ssikit.go | 9 ++++ verifier/verifiable_credential.go | 40 ++++++++++++++++ verifier/verifier.go | 80 ++++++++++++++++++++++++------- verifier/verifier_test.go | 18 ++++++- 9 files changed, 151 insertions(+), 21 deletions(-) create mode 100644 verifier/verifiable_credential.go diff --git a/config/config.go b/config/config.go index 655de198..2ba40eb5 100644 --- a/config/config.go +++ b/config/config.go @@ -48,13 +48,15 @@ type Verifier struct { SessionExpiry int `mapstructure:"sessionExpiry" default:"30"` // scope to be used in the authentication request RequestScope string `mapstructure:"requestScope"` - + // policies that shall be checked PolicyConfig Policies `mapstructure:"policies"` } type Policies struct { - DefaultPolicies PolicyMap `mapstructure:"default"` - CertificateTypeSpecificPolicies map[string]PolicyMap `mapstructure:"certificateTypeSpecific"` + // policies that all credentials are checked against + DefaultPolicies PolicyMap `mapstructure:"default"` + // policies that used to check specific credential types. Key maps to the "credentialSubject.type" of the credential + CredentialTypeSpecificPolicies map[string]PolicyMap `mapstructure:"credentialTypeSpecific"` } type PolicyMap map[string]PolicyConfigParameters diff --git a/config/data/config_test.yaml b/config/data/config_test.yaml index 62be0776..336b5423 100644 --- a/config/data/config_test.yaml +++ b/config/data/config_test.yaml @@ -18,7 +18,7 @@ verifier: SignaturePolicy: {} TrustedIssuerRegistryPolicy: registryAddress: "waltId.com" - certificateTypeSpecific: + credentialTypeSpecific: "gx:compliance": ValidFromBeforePolicy: {} ssiKit: diff --git a/config/provider_test.go b/config/provider_test.go index 6690dc89..35c4efce 100644 --- a/config/provider_test.go +++ b/config/provider_test.go @@ -31,6 +31,19 @@ func Test_ReadConfig(t *testing.T) { TirAddress: "https://test.dev/trusted_issuer/v3/issuers/", SessionExpiry: 30, RequestScope: "", + PolicyConfig: Policies{ + DefaultPolicies: PolicyMap{ + "SignaturePolicy": {}, + "TrustedIssuerRegistryPolicy": { + "registryAddress": "waltId.com", + }, + }, + CredentialTypeSpecificPolicies: map[string]PolicyMap{ + "gx:compliance": { + "ValidFromBeforePolicy": {}, + }, + }, + }, }, SSIKit: SSIKit{ AuditorURL: "http://waltid:7003", }, diff --git a/go.mod b/go.mod index 3d2e1f9c..a6fad345 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/hellofresh/health-go/v5 v5.0.0 github.com/lestrrat-go/jwx v1.2.25 github.com/patrickmn/go-cache v2.1.0+incompatible + golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 ) require ( diff --git a/go.sum b/go.sum index bff675df..ef34f83d 100644 --- a/go.sum +++ b/go.sum @@ -390,6 +390,7 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/ssikit/ssikit.go b/ssikit/ssikit.go index c7b608c9..a69ceda1 100644 --- a/ssikit/ssikit.go +++ b/ssikit/ssikit.go @@ -31,6 +31,15 @@ type Policy struct { Argument *TirArgument `json:"argument,omitempty"` } +func CreatePolicy(name string, arguments map[string]interface{}) (policy Policy) { + policy = Policy{name, nil} + if len(arguments) > 0 { + policy.Argument = &TirArgument{} + // FIXME do we need to model the arguments or is a map ok? + } + return +} + // TrustedIssuerRegistry Policy Argument - has to be provided to waltId type TirArgument struct { RegistryAddress string `json:"registryAddress"` diff --git a/verifier/verifiable_credential.go b/verifier/verifiable_credential.go new file mode 100644 index 00000000..ee0c4dab --- /dev/null +++ b/verifier/verifiable_credential.go @@ -0,0 +1,40 @@ +package verifier + +import "github.com/mitchellh/mapstructure" + +type VerifiableCredential struct { + id string `mapstructure:"id"` + types []string `mapstructure:"type"` + credentialSubject CredentialSubject `mapstructure:"credentialSubject"` + raw map[string]interface{} +} + +type CredentialSubject struct { + id string `mapstructure:"id"` + subjectType string `mapstructure:"type"` +} + +func (vc VerifiableCredential) GetCredentialType() string { + return vc.credentialSubject.subjectType +} + +func (vc VerifiableCredential) GetRawData() map[string]interface{} { + return vc.raw +} + +func MapVerifiableCredential(raw map[string]interface{}) (VerifiableCredential, error) { + var data VerifiableCredential + config := &mapstructure.DecoderConfig{ + ErrorUnused: false, + Result: &data, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return VerifiableCredential{}, err + } + if err := decoder.Decode(raw); err != nil { + return VerifiableCredential{}, err + } + data.raw = raw + return data, nil +} diff --git a/verifier/verifier.go b/verifier/verifier.go index 125d4fdb..0b86d4c4 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -13,6 +13,7 @@ import ( "time" configModel "github.com/fiware/VCVerifier/config" + "golang.org/x/exp/maps" logging "github.com/fiware/VCVerifier/logging" @@ -48,6 +49,48 @@ type Verifier interface { AuthenticationResponse(state string, verifiableCredentials []map[string]interface{}, holder string) (sameDevice SameDeviceResponse, err error) } +type ExternalVerificationService interface { + // Verifies the given VC. FIXME Currently a positiv result is returned even when no policy was checked + VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) +} + +type SsiKitExternalVerifier struct { + policies PolicyMap + credentialSpecificPolicies map[string]PolicyMap + // client for connection waltId + ssiKitClient ssikit.SSIKit +} + +type PolicyMap map[string]ssikit.Policy + +func InitSsiKitExternalVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIKit) (verifier SsiKitExternalVerifier, err error) { + defaultPolicies := PolicyMap{} + for policyName, arguments := range verifierConfig.PolicyConfig.DefaultPolicies { + defaultPolicies[policyName] = ssikit.CreatePolicy(policyName, arguments) + } + credentialSpecificPolicies := map[string]PolicyMap{} + for i, j := range verifierConfig.PolicyConfig.CredentialTypeSpecificPolicies { + credentialSpecificPolicies[i] = PolicyMap{} + for policyName, arguments := range j { + defaultPolicies[policyName] = ssikit.CreatePolicy(policyName, arguments) + } + } + return SsiKitExternalVerifier{defaultPolicies, credentialSpecificPolicies, ssiKitClient}, nil +} + +func (v *SsiKitExternalVerifier) VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) { + usedPolicies := PolicyMap{} + for name, policy := range v.policies { + usedPolicies[name] = policy + } + if policies, ok := v.credentialSpecificPolicies[verifiableCredential.GetCredentialType()]; ok { + for name, policy := range policies { + usedPolicies[name] = policy + } + } + return v.ssiKitClient.VerifyVC(maps.Values(usedPolicies), verifiableCredential.GetRawData()) +} + // implementation of the verifier, using waltId ssikit as a validation backend. type SsiKitVerifier struct { // did of the verifier @@ -56,12 +99,8 @@ type SsiKitVerifier struct { tirAddress string // optional scope of credentials to be requested scope string - // array of policies to be verified - currently statically filled on init - policies []ssikit.Policy // key to sign the jwt's with signingKey jwk.Key - // client for connection waltId - ssiKitClient ssikit.SSIKit // cache to be used for in-progress authentication sessions sessionCache Cache // cache to be used for jwt retrieval @@ -72,6 +111,8 @@ type SsiKitVerifier struct { clock Clock // provides the capabilities to signt the jwt tokenSigner TokenSigner + + verificationServices []ExternalVerificationService } // allow singleton access to the verifier @@ -171,21 +212,20 @@ func InitVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIK sessionCache := cache.New(time.Duration(verifierConfig.SessionExpiry)*time.Second, time.Duration(2*verifierConfig.SessionExpiry)*time.Second) tokenCache := cache.New(time.Duration(verifierConfig.SessionExpiry)*time.Second, time.Duration(2*verifierConfig.SessionExpiry)*time.Second) - policies := []ssikit.Policy{ - {Policy: "SignaturePolicy"}, - {Policy: "IssuedDateBeforePolicy"}, - {Policy: "ValidFromBeforePolicy"}, - {Policy: "ExpirationDateAfterPolicy"}, - {Policy: "EbsiTrustedIssuerRegistryPolicy", Argument: &ssikit.TirArgument{RegistryAddress: verifierConfig.TirAddress, IssuerType: "Undefined"}}, + externalSsiKitVerifier, err := InitSsiKitExternalVerifier(verifierConfig, ssiKitClient) + if err != nil { + logging.Log().Errorf("Was not able to initiate a external verifier. Err: %v", err) + return err } + key, err := initPrivateKey() if err != nil { logging.Log().Errorf("Was not able to initiate a signing key. Err: %v", err) return err } logging.Log().Warnf("Initiated key %s.", logging.PrettyPrintObject(key)) - verifier = &SsiKitVerifier{verifierConfig.Did, verifierConfig.TirAddress, verifierConfig.RequestScope, policies, key, ssiKitClient, sessionCache, tokenCache, &randomGenerator{}, realClock{}, jwtTokenSigner{}} + verifier = &SsiKitVerifier{verifierConfig.Did, verifierConfig.TirAddress, verifierConfig.RequestScope, key, sessionCache, tokenCache, &randomGenerator{}, realClock{}, jwtTokenSigner{}, []ExternalVerificationService{&externalSsiKitVerifier}} logging.Log().Debug("Successfully initalized the verifier") return @@ -297,14 +337,22 @@ func (v *SsiKitVerifier) AuthenticationResponse(state string, verifiableCredenti loginSession := loginSessionInterface.(loginSession) for _, vc := range verifiableCredentials { - result, err := v.ssiKitClient.VerifyVC(v.policies, vc) + mappedCredential, err := MapVerifiableCredential(vc) if err != nil { - logging.Log().Warnf("Failed to verify credential %s. Err: %v", logging.PrettyPrintObject(vc), err) + logging.Log().Warnf("Failed to map credential %s. Err: %v", logging.PrettyPrintObject(vc), err) return sameDevice, err } - if !result { - logging.Log().Infof("VC %s is not valid.", logging.PrettyPrintObject(vc)) - return sameDevice, ErrorInvalidVC + //FIXME make it an error if no policy was checked at all( possible misconfiguration) + for _, verificationService := range v.verificationServices { + result, err := verificationService.VerifyVC(mappedCredential) + if err != nil { + logging.Log().Warnf("Failed to verify credential %s. Err: %v", logging.PrettyPrintObject(vc), err) + return sameDevice, err + } + if !result { + logging.Log().Infof("VC %s is not valid.", logging.PrettyPrintObject(vc)) + return sameDevice, ErrorInvalidVC + } } } diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 21bbcb29..948d6fd4 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -219,6 +219,22 @@ func TestStartSameDeviceFlow(t *testing.T) { } +type mockExternalSsiKit struct { + verificationResults []bool + verificationError error +} + +func (msk *mockExternalSsiKit) VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) { + if msk.verificationError != nil { + return result, msk.verificationError + } + result = msk.verificationResults[0] + copy(msk.verificationResults[0:], msk.verificationResults[1:]) + msk.verificationResults[len(msk.verificationResults)-1] = false + msk.verificationResults = msk.verificationResults[:len(msk.verificationResults)-1] + return +} + type mockSsiKit struct { verificationResults []bool verificationError error @@ -321,7 +337,7 @@ func TestAuthenticationResponse(t *testing.T) { testKey, _ := jwk.New(ecdsKey) jwk.AssignKeyID(testKey) nonceGenerator := mockNonceGenerator{staticValues: []string{"authCode"}} - verifier := SsiKitVerifier{did: "did:key:verifier", signingKey: testKey, tokenCache: &tokenCache, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, ssiKitClient: &mockSsiKit{tc.verificationResult, tc.verificationError}, clock: mockClock{}} + verifier := SsiKitVerifier{did: "did:key:verifier", signingKey: testKey, tokenCache: &tokenCache, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, verificationServices: []ExternalVerificationService{&mockExternalSsiKit{tc.verificationResult, tc.verificationError}}, clock: mockClock{}} sameDeviceResponse, err := verifier.AuthenticationResponse(tc.requestedState, tc.testVC, tc.testHolder) if err != tc.expectedError { From 6f30368c3328f330adfcf0fd23690b7bfe967d2c Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Thu, 20 Apr 2023 13:33:57 +0200 Subject: [PATCH 03/12] cleanup --- server.yaml | 9 ++++++- ssikit/ssikit.go | 11 ++++---- ssikit/ssikit_test.go | 45 +++++++++++++++++++++++++++++++ verifier/verifiable_credential.go | 3 +++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/server.yaml b/server.yaml index a78dc2f4..ef20d929 100644 --- a/server.yaml +++ b/server.yaml @@ -7,6 +7,13 @@ logging: verifier: tirAddress: https://tir.de did: did:key:myverifier - + policies: + default: + SignaturePolicy: {} + IssuedDateBeforePolicy: {} + ValidFromBeforePolicy: {} + ExpirationDateAfterPolicy: {} + TrustedIssuerRegistryPolicy: + registryAddress: https://tir.de ssiKit: auditorURL: http://my-auditor \ No newline at end of file diff --git a/ssikit/ssikit.go b/ssikit/ssikit.go index a69ceda1..bcf90769 100644 --- a/ssikit/ssikit.go +++ b/ssikit/ssikit.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "net/http" configModel "github.com/fiware/VCVerifier/config" @@ -31,20 +32,20 @@ type Policy struct { Argument *TirArgument `json:"argument,omitempty"` } +// Create a policy as defined by waltId FIXME filter out policies that are not covered by waltId func CreatePolicy(name string, arguments map[string]interface{}) (policy Policy) { policy = Policy{name, nil} if len(arguments) > 0 { policy.Argument = &TirArgument{} - // FIXME do we need to model the arguments or is a map ok? + for name, value := range arguments { + (*policy.Argument)[name] = fmt.Sprintf("%v", value) + } } return } // TrustedIssuerRegistry Policy Argument - has to be provided to waltId -type TirArgument struct { - RegistryAddress string `json:"registryAddress"` - IssuerType string `json:"issuerType"` -} +type TirArgument map[string]string // request structure for validating VCs at waltId type verificationRequest struct { diff --git a/ssikit/ssikit_test.go b/ssikit/ssikit_test.go index 22e08d08..4e5e1c93 100644 --- a/ssikit/ssikit_test.go +++ b/ssikit/ssikit_test.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/url" + "reflect" "testing" configModel "github.com/fiware/VCVerifier/config" @@ -128,3 +129,47 @@ func getVC(id string) map[string]interface{} { }, } } + +func TestCreatePolicy(t *testing.T) { + type args struct { + name string + arguments map[string]interface{} + } + tests := []struct { + name string + args args + wantPolicy Policy + }{ + { + "Policy without arguments", + args{ + "testPolicy", + map[string]interface{}{}, + }, + Policy{ + "testPolicy", + nil, + }, + }, + { + "Policy with arguments", + args{ + "testPolicy", + map[string]interface{}{ + "arg1":"something", + }, + }, + Policy{ + "testPolicy", + &TirArgument{"arg1":"something"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotPolicy := CreatePolicy(tt.args.name, tt.args.arguments); !reflect.DeepEqual(gotPolicy, tt.wantPolicy) { + t.Errorf("CreatePolicy() = %v, want %v", gotPolicy, tt.wantPolicy) + } + }) + } +} diff --git a/verifier/verifiable_credential.go b/verifier/verifiable_credential.go index ee0c4dab..0ed2c1e1 100644 --- a/verifier/verifiable_credential.go +++ b/verifier/verifiable_credential.go @@ -2,13 +2,16 @@ package verifier import "github.com/mitchellh/mapstructure" +// Subset of the structure of a Verifiable Credential type VerifiableCredential struct { id string `mapstructure:"id"` types []string `mapstructure:"type"` credentialSubject CredentialSubject `mapstructure:"credentialSubject"` + // The unaltered complete credential raw map[string]interface{} } +// Subset of the structure of a CredentialSubject inside a Verifiable Credential type CredentialSubject struct { id string `mapstructure:"id"` subjectType string `mapstructure:"type"` From 0bf3679c50514cc27ec08b78c752a2b8d540c218 Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Thu, 20 Apr 2023 23:35:26 +0200 Subject: [PATCH 04/12] add gaiax verifier --- gaiax/registry.go | 56 +++++++++++++++++++++ verifier/verifiable_credential.go | 23 +++++---- verifier/verifiable_credential_test.go | 69 ++++++++++++++++++++++++++ verifier/verifier.go | 52 ++++++++++++++++++- 4 files changed, 190 insertions(+), 10 deletions(-) create mode 100644 gaiax/registry.go create mode 100644 verifier/verifiable_credential_test.go diff --git a/gaiax/registry.go b/gaiax/registry.go new file mode 100644 index 00000000..ba02831f --- /dev/null +++ b/gaiax/registry.go @@ -0,0 +1,56 @@ +package gaiax + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/fiware/VCVerifier/logging" +) + +var ErrorRegistryNoResponse = errors.New("no_response_from_gaiax_registry") + +type RegistryClient interface { + // Get the list of DIDs of the trustable issuers + GetComplianceIssuers() ([]string, error) +} + +type GaiaXRegistryClient struct { + endpoint string +} + +func InitGaiaXRegistryVerifier(url string) RegistryClient{ + return &GaiaXRegistryClient{url} +} + +// TODO Could propably cache the response very generously as new issuers are not added often +func (rc *GaiaXRegistryClient) GetComplianceIssuers() ([]string, error) { + response, err := http.Get(rc.endpoint) + + if err != nil { + logging.Log().Warnf("Did not receive a valid issuers list response. Err: %v", err) + return []string{}, err + } + if response == nil { + logging.Log().Warn("Did not receive any response from gaia-x registry.") + return []string{}, ErrorRegistryNoResponse + } + if response.StatusCode != 200 { + logging.Log().Infof("Did not receive an ok from the registry. Was %s", logging.PrettyPrintObject(response)) + return []string{}, err + } + if response.Body == nil { + logging.Log().Info("Received an empty body for the issuers list.") + return []string{}, err + } + var issuers []string + + err = json.NewDecoder(response.Body).Decode(&issuers) + if err != nil { + logging.Log().Warn("Was not able to decode the issuers list.") + return []string{}, err + } + logging.Log().Info("%d issuer dids received.", len(issuers)) + logging.Log().Debugf("Issuers are %v", logging.PrettyPrintObject(issuers)) + return issuers, nil +} diff --git a/verifier/verifiable_credential.go b/verifier/verifiable_credential.go index 0ed2c1e1..7179e2f8 100644 --- a/verifier/verifiable_credential.go +++ b/verifier/verifiable_credential.go @@ -4,25 +4,30 @@ import "github.com/mitchellh/mapstructure" // Subset of the structure of a Verifiable Credential type VerifiableCredential struct { - id string `mapstructure:"id"` - types []string `mapstructure:"type"` - credentialSubject CredentialSubject `mapstructure:"credentialSubject"` + Id string `mapstructure:"id"` + Types []string `mapstructure:"type"` + Issuer string `mapstructure:"issuer"` + CredentialSubject CredentialSubject `mapstructure:"credentialSubject"` // The unaltered complete credential - raw map[string]interface{} + Raw map[string]interface{} } // Subset of the structure of a CredentialSubject inside a Verifiable Credential type CredentialSubject struct { - id string `mapstructure:"id"` - subjectType string `mapstructure:"type"` + Id string `mapstructure:"id"` + SubjectType string `mapstructure:"type"` } func (vc VerifiableCredential) GetCredentialType() string { - return vc.credentialSubject.subjectType + return vc.CredentialSubject.SubjectType } func (vc VerifiableCredential) GetRawData() map[string]interface{} { - return vc.raw + return vc.Raw +} + +func (vc VerifiableCredential) GetIssuer() string { + return vc.Issuer } func MapVerifiableCredential(raw map[string]interface{}) (VerifiableCredential, error) { @@ -38,6 +43,6 @@ func MapVerifiableCredential(raw map[string]interface{}) (VerifiableCredential, if err := decoder.Decode(raw); err != nil { return VerifiableCredential{}, err } - data.raw = raw + data.Raw = raw return data, nil } diff --git a/verifier/verifiable_credential_test.go b/verifier/verifiable_credential_test.go new file mode 100644 index 00000000..44152cd4 --- /dev/null +++ b/verifier/verifiable_credential_test.go @@ -0,0 +1,69 @@ +package verifier + +import ( + "reflect" + "testing" +) + +var exampleCredential = map[string]interface{}{ + "@context": []string{ + "https://www.w3.org/2018/credentials/v1", + "https://happypets.fiware.io/2022/credentials/employee/v1", + }, + "id": "https://happypets.fiware.io/credential/25159389-8dd17b796ac0", + "type": []string{ + "VerifiableCredential", + "CustomerCredential", + }, + "issuer": "did:key:verifier", + "issuanceDate": "2022-11-23T15:23:13Z", + "validFrom": "2022-11-23T15:23:13Z", + "expirationDate": "2032-11-23T15:23:13Z", + "credentialSubject": map[string]interface{}{ + "id": "someId", + "target": "did:ebsi:packetdelivery", + "type": "gx:compliance", + }, +} + +func TestMapVerifiableCredential(t *testing.T) { + type args struct { + raw map[string]interface{} + } + tests := []struct { + name string + args args + want VerifiableCredential + wantErr bool + }{ + { + "ValidCertificate", + args{exampleCredential}, + VerifiableCredential{ + Id: "https://happypets.fiware.io/credential/25159389-8dd17b796ac0", + Types: []string{ + "VerifiableCredential", + "CustomerCredential", + }, + Raw: exampleCredential, + CredentialSubject: CredentialSubject{ + Id: "someId", + SubjectType: "gx:compliance", + }, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MapVerifiableCredential(tt.args.raw) + if (err != nil) != tt.wantErr { + t.Errorf("MapVerifiableCredential() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MapVerifiableCredential() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/verifier/verifier.go b/verifier/verifier.go index 0b86d4c4..940400e9 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -13,7 +13,9 @@ import ( "time" configModel "github.com/fiware/VCVerifier/config" + "github.com/fiware/VCVerifier/gaiax" "golang.org/x/exp/maps" + "golang.org/x/exp/slices" logging "github.com/fiware/VCVerifier/logging" @@ -61,6 +63,55 @@ type SsiKitExternalVerifier struct { ssiKitClient ssikit.SSIKit } +type GaiaXRegistryVerifier struct { + validateAll bool + credentialTypesToValidate []string + // client for gaiax registry connection + gaiaxRegistryClient gaiax.RegistryClient +} + +func InitGaiaXRegistryVerifier(verifierConfig *configModel.Verifier) GaiaXRegistryVerifier { + var url string + verifier := GaiaXRegistryVerifier{credentialTypesToValidate: []string{}} + + for policyName, arguments := range verifierConfig.PolicyConfig.DefaultPolicies { + if policyName == "gaiaxcomplianceissuer" { + url = fmt.Sprintf("%v", arguments["registryUrl"]) + verifier.validateAll = true + } + } + for credentialType, policies := range verifierConfig.PolicyConfig.CredentialTypeSpecificPolicies { + for policyName, arguments := range policies { + if policyName == "gaiaxcomplianceissuer" { + url = fmt.Sprintf("%v", arguments["registryUrl"]) + verifier.credentialTypesToValidate = append(verifier.credentialTypesToValidate, credentialType) + } + } + } + if len(url) > 0 { + verifier.gaiaxRegistryClient = gaiax.InitGaiaXRegistryVerifier(url) + } + return verifier +} + +func (v *GaiaXRegistryVerifier) VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) { + if v.validateAll || slices.Contains(v.credentialTypesToValidate, verifiableCredential.GetCredentialType()) { + issuerDids, err := v.gaiaxRegistryClient.GetComplianceIssuers() + if err != nil { + return false, err + } + if slices.Contains(issuerDids, verifiableCredential.GetIssuer()) { + logging.Log().Info("Credential was issued by trusted issuer") + return true, nil + } else { + logging.Log().Warnf("Failed to verify credential %s. Issuer was not in trusted issuer list", logging.PrettyPrintObject(verifiableCredential), err) + return false, nil + } + } + // No need to validate + return true, nil +} + type PolicyMap map[string]ssikit.Policy func InitSsiKitExternalVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIKit) (verifier SsiKitExternalVerifier, err error) { @@ -218,7 +269,6 @@ func InitVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIK return err } - key, err := initPrivateKey() if err != nil { logging.Log().Errorf("Was not able to initiate a signing key. Err: %v", err) From af92c6a19affa023ca1785fb5f0abcac42f03bb9 Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Fri, 21 Apr 2023 13:03:08 +0200 Subject: [PATCH 05/12] cleanup --- gaiax/registry.go | 18 ++-- gaiax/registry_test.go | 80 +++++++++++++++ server.yaml | 9 +- verifier/gaiax.go | 63 ++++++++++++ verifier/ssikit.go | 54 ++++++++++ verifier/verifiable_credential.go | 11 +-- verifier/verifiable_credential_test.go | 3 +- verifier/verifier.go | 130 ++++++------------------- verifier/verifier_test.go | 12 +-- 9 files changed, 255 insertions(+), 125 deletions(-) create mode 100644 gaiax/registry_test.go create mode 100644 verifier/gaiax.go create mode 100644 verifier/ssikit.go diff --git a/gaiax/registry.go b/gaiax/registry.go index ba02831f..104ffe76 100644 --- a/gaiax/registry.go +++ b/gaiax/registry.go @@ -8,7 +8,7 @@ import ( "github.com/fiware/VCVerifier/logging" ) -var ErrorRegistryNoResponse = errors.New("no_response_from_gaiax_registry") +var ErrorRegistry = errors.New("gaiax_registry_failed_to_answer_properly") type RegistryClient interface { // Get the list of DIDs of the trustable issuers @@ -19,7 +19,7 @@ type GaiaXRegistryClient struct { endpoint string } -func InitGaiaXRegistryVerifier(url string) RegistryClient{ +func InitGaiaXRegistryVerifier(url string) RegistryClient { return &GaiaXRegistryClient{url} } @@ -33,24 +33,24 @@ func (rc *GaiaXRegistryClient) GetComplianceIssuers() ([]string, error) { } if response == nil { logging.Log().Warn("Did not receive any response from gaia-x registry.") - return []string{}, ErrorRegistryNoResponse + return []string{}, ErrorRegistry } if response.StatusCode != 200 { - logging.Log().Infof("Did not receive an ok from the registry. Was %s", logging.PrettyPrintObject(response)) - return []string{}, err + logging.Log().Warnf("Did not receive an ok from the registry. Was %s", logging.PrettyPrintObject(response)) + return []string{}, ErrorRegistry } if response.Body == nil { - logging.Log().Info("Received an empty body for the issuers list.") - return []string{}, err + logging.Log().Warn("Received an empty body for the issuers list.") + return []string{}, ErrorRegistry } var issuers []string err = json.NewDecoder(response.Body).Decode(&issuers) if err != nil { - logging.Log().Warn("Was not able to decode the issuers list.") + logging.Log().Warnf("Was not able to decode the issuers list. Was %s", logging.PrettyPrintObject(response)) return []string{}, err } - logging.Log().Info("%d issuer dids received.", len(issuers)) + logging.Log().Infof("%d issuer dids received.", len(issuers)) logging.Log().Debugf("Issuers are %v", logging.PrettyPrintObject(issuers)) return issuers, nil } diff --git a/gaiax/registry_test.go b/gaiax/registry_test.go new file mode 100644 index 00000000..7ba4f797 --- /dev/null +++ b/gaiax/registry_test.go @@ -0,0 +1,80 @@ +package gaiax + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestGaiaXRegistryClient_GetComplianceIssuers(t *testing.T) { + + type fields struct { + response string + responseCode int + } + tests := []struct { + name string + fields fields + want []string + wantErr bool + }{ + { + "Should return one did", + fields{ + `["did:web:compliance.test.com"]`, + 200, + }, + []string{"did:web:compliance.test.com"}, + false, + }, + { + "Should return multiple dids", + fields{ + `["did:web:compliance.test.com","did:key:123"]`, + 200, + }, + []string{"did:web:compliance.test.com", "did:key:123"}, + false, + }, + { + "Should return error when malformatted", + fields{ + `{"someThing":"else"}`, + 200, + }, + []string{}, + true, + }, + { + "Should return error when http error", + fields{ + ``, + 500, + }, + []string{}, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.fields.responseCode) + w.Write([]byte(tt.fields.response)) + })) + defer server.Close() + + rc := &GaiaXRegistryClient{ + endpoint: server.URL, + } + got, err := rc.GetComplianceIssuers() + if (err != nil) != tt.wantErr { + t.Errorf("GaiaXRegistryClient.GetComplianceIssuers() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GaiaXRegistryClient.GetComplianceIssuers() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/server.yaml b/server.yaml index ef20d929..de17d24e 100644 --- a/server.yaml +++ b/server.yaml @@ -15,5 +15,12 @@ verifier: ExpirationDateAfterPolicy: {} TrustedIssuerRegistryPolicy: registryAddress: https://tir.de + credentialTypeSpecific: + "gx:compliance": + GaiaXComplianceIssuer: + registryAddress: https://registry.gaia-x.fiware.dev/development/api/complianceIssuers ssiKit: - auditorURL: http://my-auditor \ No newline at end of file + auditorURL: http://my-auditor + + + \ No newline at end of file diff --git a/verifier/gaiax.go b/verifier/gaiax.go new file mode 100644 index 00000000..e7adc7b4 --- /dev/null +++ b/verifier/gaiax.go @@ -0,0 +1,63 @@ +package verifier + +import ( + "fmt" + + configModel "github.com/fiware/VCVerifier/config" + "github.com/fiware/VCVerifier/gaiax" + "golang.org/x/exp/slices" + + logging "github.com/fiware/VCVerifier/logging" +) + +const gaiaxCompliancePolicy = "GaiaXComplianceIssuer" +const registryUrlPropertyName = "registryAddress" + +type GaiaXRegistryVerifier struct { + validateAll bool + credentialTypesToValidate []string + // client for gaiax registry connection + gaiaxRegistryClient gaiax.RegistryClient +} + +func InitGaiaXRegistryVerifier(verifierConfig *configModel.Verifier) GaiaXRegistryVerifier { + var url string + verifier := GaiaXRegistryVerifier{credentialTypesToValidate: []string{}} + + for policyName, arguments := range verifierConfig.PolicyConfig.DefaultPolicies { + if policyName == gaiaxCompliancePolicy { + url = fmt.Sprintf("%v", arguments[registryUrlPropertyName]) + verifier.validateAll = true + } + } + for credentialType, policies := range verifierConfig.PolicyConfig.CredentialTypeSpecificPolicies { + for policyName, arguments := range policies { + if policyName == gaiaxCompliancePolicy { + url = fmt.Sprintf("%v", arguments[registryUrlPropertyName]) + verifier.credentialTypesToValidate = append(verifier.credentialTypesToValidate, credentialType) + } + } + } + if len(url) > 0 { + verifier.gaiaxRegistryClient = gaiax.InitGaiaXRegistryVerifier(url) + } + return verifier +} + +func (v *GaiaXRegistryVerifier) VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) { + if v.validateAll || slices.Contains(v.credentialTypesToValidate, verifiableCredential.GetCredentialType()) { + issuerDids, err := v.gaiaxRegistryClient.GetComplianceIssuers() + if err != nil { + return false, err + } + if slices.Contains(issuerDids, verifiableCredential.GetIssuer()) { + logging.Log().Info("Credential was issued by trusted issuer") + return true, nil + } else { + logging.Log().Warnf("Failed to verify credential %s. Issuer was not in trusted issuer list", logging.PrettyPrintObject(verifiableCredential)) + return false, nil + } + } + // No need to validate + return true, nil +} diff --git a/verifier/ssikit.go b/verifier/ssikit.go new file mode 100644 index 00000000..19ee88eb --- /dev/null +++ b/verifier/ssikit.go @@ -0,0 +1,54 @@ +package verifier + +import ( + configModel "github.com/fiware/VCVerifier/config" + "golang.org/x/exp/maps" + + "github.com/fiware/VCVerifier/ssikit" +) + +type SsiKitExternalVerifier struct { + policies PolicyMap + credentialSpecificPolicies map[string]PolicyMap + // client for connection waltId + ssiKitClient ssikit.SSIKit +} + +type PolicyMap map[string]ssikit.Policy + +// checks if the policy should be handled by ssikit +func isPolicySupportedBySsiKit(policyName string) bool { + return policyName != gaiaxCompliancePolicy +} + +func InitSsiKitExternalVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIKit) (verifier SsiKitExternalVerifier, err error) { + defaultPolicies := PolicyMap{} + for policyName, arguments := range verifierConfig.PolicyConfig.DefaultPolicies { + if isPolicySupportedBySsiKit(policyName) { + defaultPolicies[policyName] = ssikit.CreatePolicy(policyName, arguments) + } + } + credentialSpecificPolicies := map[string]PolicyMap{} + for i, j := range verifierConfig.PolicyConfig.CredentialTypeSpecificPolicies { + credentialSpecificPolicies[i] = PolicyMap{} + for policyName, arguments := range j { + if isPolicySupportedBySsiKit(policyName) { + defaultPolicies[policyName] = ssikit.CreatePolicy(policyName, arguments) + } + } + } + return SsiKitExternalVerifier{defaultPolicies, credentialSpecificPolicies, ssiKitClient}, nil +} + +func (v *SsiKitExternalVerifier) VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) { + usedPolicies := PolicyMap{} + for name, policy := range v.policies { + usedPolicies[name] = policy + } + if policies, ok := v.credentialSpecificPolicies[verifiableCredential.GetCredentialType()]; ok { + for name, policy := range policies { + usedPolicies[name] = policy + } + } + return v.ssiKitClient.VerifyVC(maps.Values(usedPolicies), verifiableCredential.GetRawData()) +} diff --git a/verifier/verifiable_credential.go b/verifier/verifiable_credential.go index 7179e2f8..79684c30 100644 --- a/verifier/verifiable_credential.go +++ b/verifier/verifiable_credential.go @@ -4,12 +4,11 @@ import "github.com/mitchellh/mapstructure" // Subset of the structure of a Verifiable Credential type VerifiableCredential struct { - Id string `mapstructure:"id"` - Types []string `mapstructure:"type"` - Issuer string `mapstructure:"issuer"` - CredentialSubject CredentialSubject `mapstructure:"credentialSubject"` - // The unaltered complete credential - Raw map[string]interface{} + Id string `mapstructure:"id"` + Types []string `mapstructure:"type"` + Issuer string `mapstructure:"issuer"` + CredentialSubject CredentialSubject `mapstructure:"credentialSubject"` + Raw map[string]interface{} // The unaltered complete credential } // Subset of the structure of a CredentialSubject inside a Verifiable Credential diff --git a/verifier/verifiable_credential_test.go b/verifier/verifiable_credential_test.go index 44152cd4..2c999766 100644 --- a/verifier/verifiable_credential_test.go +++ b/verifier/verifiable_credential_test.go @@ -45,7 +45,8 @@ func TestMapVerifiableCredential(t *testing.T) { "VerifiableCredential", "CustomerCredential", }, - Raw: exampleCredential, + Issuer: "did:key:verifier", + Raw: exampleCredential, CredentialSubject: CredentialSubject{ Id: "someId", SubjectType: "gx:compliance", diff --git a/verifier/verifier.go b/verifier/verifier.go index 940400e9..41557323 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -13,9 +13,6 @@ import ( "time" configModel "github.com/fiware/VCVerifier/config" - "github.com/fiware/VCVerifier/gaiax" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" logging "github.com/fiware/VCVerifier/logging" @@ -56,94 +53,8 @@ type ExternalVerificationService interface { VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) } -type SsiKitExternalVerifier struct { - policies PolicyMap - credentialSpecificPolicies map[string]PolicyMap - // client for connection waltId - ssiKitClient ssikit.SSIKit -} - -type GaiaXRegistryVerifier struct { - validateAll bool - credentialTypesToValidate []string - // client for gaiax registry connection - gaiaxRegistryClient gaiax.RegistryClient -} - -func InitGaiaXRegistryVerifier(verifierConfig *configModel.Verifier) GaiaXRegistryVerifier { - var url string - verifier := GaiaXRegistryVerifier{credentialTypesToValidate: []string{}} - - for policyName, arguments := range verifierConfig.PolicyConfig.DefaultPolicies { - if policyName == "gaiaxcomplianceissuer" { - url = fmt.Sprintf("%v", arguments["registryUrl"]) - verifier.validateAll = true - } - } - for credentialType, policies := range verifierConfig.PolicyConfig.CredentialTypeSpecificPolicies { - for policyName, arguments := range policies { - if policyName == "gaiaxcomplianceissuer" { - url = fmt.Sprintf("%v", arguments["registryUrl"]) - verifier.credentialTypesToValidate = append(verifier.credentialTypesToValidate, credentialType) - } - } - } - if len(url) > 0 { - verifier.gaiaxRegistryClient = gaiax.InitGaiaXRegistryVerifier(url) - } - return verifier -} - -func (v *GaiaXRegistryVerifier) VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) { - if v.validateAll || slices.Contains(v.credentialTypesToValidate, verifiableCredential.GetCredentialType()) { - issuerDids, err := v.gaiaxRegistryClient.GetComplianceIssuers() - if err != nil { - return false, err - } - if slices.Contains(issuerDids, verifiableCredential.GetIssuer()) { - logging.Log().Info("Credential was issued by trusted issuer") - return true, nil - } else { - logging.Log().Warnf("Failed to verify credential %s. Issuer was not in trusted issuer list", logging.PrettyPrintObject(verifiableCredential), err) - return false, nil - } - } - // No need to validate - return true, nil -} - -type PolicyMap map[string]ssikit.Policy - -func InitSsiKitExternalVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIKit) (verifier SsiKitExternalVerifier, err error) { - defaultPolicies := PolicyMap{} - for policyName, arguments := range verifierConfig.PolicyConfig.DefaultPolicies { - defaultPolicies[policyName] = ssikit.CreatePolicy(policyName, arguments) - } - credentialSpecificPolicies := map[string]PolicyMap{} - for i, j := range verifierConfig.PolicyConfig.CredentialTypeSpecificPolicies { - credentialSpecificPolicies[i] = PolicyMap{} - for policyName, arguments := range j { - defaultPolicies[policyName] = ssikit.CreatePolicy(policyName, arguments) - } - } - return SsiKitExternalVerifier{defaultPolicies, credentialSpecificPolicies, ssiKitClient}, nil -} - -func (v *SsiKitExternalVerifier) VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) { - usedPolicies := PolicyMap{} - for name, policy := range v.policies { - usedPolicies[name] = policy - } - if policies, ok := v.credentialSpecificPolicies[verifiableCredential.GetCredentialType()]; ok { - for name, policy := range policies { - usedPolicies[name] = policy - } - } - return v.ssiKitClient.VerifyVC(maps.Values(usedPolicies), verifiableCredential.GetRawData()) -} - -// implementation of the verifier, using waltId ssikit as a validation backend. -type SsiKitVerifier struct { +// implementation of the verifier, using waltId ssikit and gaia-x compliance issuers registry as a validation backends. +type CredentialVerifier struct { // did of the verifier did string // trusted-issuers-registry to be used for verification @@ -162,7 +73,7 @@ type SsiKitVerifier struct { clock Clock // provides the capabilities to signt the jwt tokenSigner TokenSigner - + // Verification services to be used on the credentials verificationServices []ExternalVerificationService } @@ -268,6 +179,7 @@ func InitVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIK logging.Log().Errorf("Was not able to initiate a external verifier. Err: %v", err) return err } + externalGaiaXVerifier := InitGaiaXRegistryVerifier(verifierConfig) key, err := initPrivateKey() if err != nil { @@ -275,7 +187,21 @@ func InitVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIK return err } logging.Log().Warnf("Initiated key %s.", logging.PrettyPrintObject(key)) - verifier = &SsiKitVerifier{verifierConfig.Did, verifierConfig.TirAddress, verifierConfig.RequestScope, key, sessionCache, tokenCache, &randomGenerator{}, realClock{}, jwtTokenSigner{}, []ExternalVerificationService{&externalSsiKitVerifier}} + verifier = &CredentialVerifier{ + verifierConfig.Did, + verifierConfig.TirAddress, + verifierConfig.RequestScope, + key, + sessionCache, + tokenCache, + &randomGenerator{}, + realClock{}, + jwtTokenSigner{}, + []ExternalVerificationService{ + &externalSsiKitVerifier, + &externalGaiaXVerifier, + }, + } logging.Log().Debug("Successfully initalized the verifier") return @@ -284,7 +210,7 @@ func InitVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIK /** * Initializes the cross-device login flow and returns all neccessary information as a qr-code **/ -func (v *SsiKitVerifier) ReturnLoginQR(host string, protocol string, callback string, sessionId string) (qr string, err error) { +func (v *CredentialVerifier) ReturnLoginQR(host string, protocol string, callback string, sessionId string) (qr string, err error) { logging.Log().Debugf("Generate a login qr for %s.", callback) authenticationRequest, err := v.initSiopFlow(host, protocol, callback, sessionId) @@ -303,7 +229,7 @@ func (v *SsiKitVerifier) ReturnLoginQR(host string, protocol string, callback st /** * Starts a siop-flow and returns the required connection information **/ -func (v *SsiKitVerifier) StartSiopFlow(host string, protocol string, callback string, sessionId string) (connectionString string, err error) { +func (v *CredentialVerifier) StartSiopFlow(host string, protocol string, callback string, sessionId string) (connectionString string, err error) { logging.Log().Debugf("Start a plain siop-flow fro %s.", callback) return v.initSiopFlow(host, protocol, callback, sessionId) @@ -312,7 +238,7 @@ func (v *SsiKitVerifier) StartSiopFlow(host string, protocol string, callback st /** * Starts a same-device siop-flow and returns the required redirection information **/ -func (v *SsiKitVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string) (authenticationRequest string, err error) { +func (v *CredentialVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string) (authenticationRequest string, err error) { logging.Log().Debugf("Initiate samedevice flow for %s.", host) state := v.nonceGenerator.GenerateNonce() @@ -332,7 +258,7 @@ func (v *SsiKitVerifier) StartSameDeviceFlow(host string, protocol string, sessi /** * Returns an already generated jwt from the cache to properly authorized requests. Every token will only be returend once. **/ -func (v *SsiKitVerifier) GetToken(grantType string, authorizationCode string, redirectUri string) (jwtString string, expiration int64, err error) { +func (v *CredentialVerifier) GetToken(grantType string, authorizationCode string, redirectUri string) (jwtString string, expiration int64, err error) { if grantType != "authorization_code" { return jwtString, expiration, ErrorWrongGrantType @@ -364,7 +290,7 @@ func (v *SsiKitVerifier) GetToken(grantType string, authorizationCode string, re /** * Return the JWKS used by the verifier to allow jwt verification **/ -func (v *SsiKitVerifier) GetJWKS() jwk.Set { +func (v *CredentialVerifier) GetJWKS() jwk.Set { jwks := jwk.NewSet() publicKey, _ := v.signingKey.PublicKey() jwks.Add(publicKey) @@ -375,7 +301,7 @@ func (v *SsiKitVerifier) GetJWKS() jwk.Set { * Receive credentials and verify them in the context of an already present login-session. Will return either an error if failed, a sameDevice response to be used for * redirection or notify the original initiator(in case of a cross-device flow) **/ -func (v *SsiKitVerifier) AuthenticationResponse(state string, verifiableCredentials []map[string]interface{}, holder string) (sameDevice SameDeviceResponse, err error) { +func (v *CredentialVerifier) AuthenticationResponse(state string, verifiableCredentials []map[string]interface{}, holder string) (sameDevice SameDeviceResponse, err error) { logging.Log().Debugf("Authenticate credential for session %s", state) @@ -432,7 +358,7 @@ func (v *SsiKitVerifier) AuthenticationResponse(state string, verifiableCredenti } // initializes the cross-device siop flow -func (v *SsiKitVerifier) initSiopFlow(host string, protocol string, callback string, sessionId string) (authenticationRequest string, err error) { +func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback string, sessionId string) (authenticationRequest string, err error) { state := v.nonceGenerator.GenerateNonce() loginSession := loginSession{false, callback, sessionId} @@ -450,7 +376,7 @@ func (v *SsiKitVerifier) initSiopFlow(host string, protocol string, callback str } // generate a jwt, containing the credential and mandatory information as defined by the dsba-convergence -func (v *SsiKitVerifier) generateJWT(verifiableCredentials []map[string]interface{}, holder string, audience string) (generatedJwt jwt.Token, err error) { +func (v *CredentialVerifier) generateJWT(verifiableCredentials []map[string]interface{}, holder string, audience string) (generatedJwt jwt.Token, err error) { jwtBuilder := jwt.NewBuilder().Issuer(v.did).Claim("client_id", v.did).Subject(holder).Audience([]string{audience}).Claim("kid", v.signingKey.KeyID()).Expiration(v.clock.Now().Add(time.Minute * 30)) if v.scope != "" { @@ -468,7 +394,7 @@ func (v *SsiKitVerifier) generateJWT(verifiableCredentials []map[string]interfac } // creates an authenticationRequest string from the given parameters -func (v *SsiKitVerifier) createAuthenticationRequest(base string, redirect_uri string, state string) string { +func (v *CredentialVerifier) createAuthenticationRequest(base string, redirect_uri string, state string) string { // We use a template to generate the final string template := "{{base}}?response_type=vp_token" + diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 948d6fd4..4c5aec98 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -129,7 +129,7 @@ func TestInitSiopFlow(t *testing.T) { logging.Log().Info("TestInitSiopFlow +++++++++++++++++ Running test: ", tc.testName) sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} - verifier := SsiKitVerifier{did: "did:key:verifier", scope: tc.scopeConfig, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator} + verifier := CredentialVerifier{did: "did:key:verifier", scope: tc.scopeConfig, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator} authReq, err := verifier.initSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId) verifyInitTest(t, tc, authReq, err, sessionCache, false) } @@ -146,7 +146,7 @@ func TestStartSiopFlow(t *testing.T) { sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} - verifier := SsiKitVerifier{did: "did:key:verifier", scope: tc.scopeConfig, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator} + verifier := CredentialVerifier{did: "did:key:verifier", scope: tc.scopeConfig, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator} authReq, err := verifier.StartSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId) verifyInitTest(t, tc, authReq, err, sessionCache, false) } @@ -212,7 +212,7 @@ func TestStartSameDeviceFlow(t *testing.T) { logging.Log().Info("TestSameDeviceFlow +++++++++++++++++ Running test: ", tc.testName) sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} - verifier := SsiKitVerifier{did: "did:key:verifier", scope: tc.scopeConfig, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator} + verifier := CredentialVerifier{did: "did:key:verifier", scope: tc.scopeConfig, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator} authReq, err := verifier.StartSameDeviceFlow(tc.testHost, tc.testProtocol, tc.testSessionId, tc.testAddress) verifyInitTest(t, tc, authReq, err, sessionCache, true) } @@ -337,7 +337,7 @@ func TestAuthenticationResponse(t *testing.T) { testKey, _ := jwk.New(ecdsKey) jwk.AssignKeyID(testKey) nonceGenerator := mockNonceGenerator{staticValues: []string{"authCode"}} - verifier := SsiKitVerifier{did: "did:key:verifier", signingKey: testKey, tokenCache: &tokenCache, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, verificationServices: []ExternalVerificationService{&mockExternalSsiKit{tc.verificationResult, tc.verificationError}}, clock: mockClock{}} + verifier := CredentialVerifier{did: "did:key:verifier", signingKey: testKey, tokenCache: &tokenCache, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, verificationServices: []ExternalVerificationService{&mockExternalSsiKit{tc.verificationResult, tc.verificationError}}, clock: mockClock{}} sameDeviceResponse, err := verifier.AuthenticationResponse(tc.requestedState, tc.testVC, tc.testHolder) if err != tc.expectedError { @@ -441,7 +441,7 @@ func TestGetJWKS(t *testing.T) { ecdsKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) testKey, _ := jwk.New(ecdsKey) - verifier := SsiKitVerifier{signingKey: testKey} + verifier := CredentialVerifier{signingKey: testKey} jwks := verifier.GetJWKS() @@ -509,7 +509,7 @@ func TestGetToken(t *testing.T) { logging.Log().Info("TestGetToken +++++++++++++++++ Running test: ", tc.testName) tokenCache := mockTokenCache{tokens: tc.tokenSession} - verifier := SsiKitVerifier{tokenCache: &tokenCache, signingKey: testKey, clock: mockClock{}, tokenSigner: mockTokenSigner{tc.signingError}} + verifier := CredentialVerifier{tokenCache: &tokenCache, signingKey: testKey, clock: mockClock{}, tokenSigner: mockTokenSigner{tc.signingError}} jwtString, expiration, err := verifier.GetToken(tc.testGrantType, tc.testCode, tc.testRedirectUri) if err != tc.expectedError { From f8e899b0abb9b67e15aa9ea6c9dad7533cf07f16 Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Mon, 24 Apr 2023 14:09:22 +0200 Subject: [PATCH 06/12] multiple credentials in jwt, check chain --- gaiax/registry_test.go | 5 +-- verifier/verifiable_credential.go | 29 ++++++++----- verifier/verifiable_credential_test.go | 28 +++++++----- verifier/verifier.go | 60 ++++++++++++++++++++++++-- verifier/verifier_test.go | 1 + 5 files changed, 96 insertions(+), 27 deletions(-) diff --git a/gaiax/registry_test.go b/gaiax/registry_test.go index 7ba4f797..f6645720 100644 --- a/gaiax/registry_test.go +++ b/gaiax/registry_test.go @@ -64,9 +64,8 @@ func TestGaiaXRegistryClient_GetComplianceIssuers(t *testing.T) { })) defer server.Close() - rc := &GaiaXRegistryClient{ - endpoint: server.URL, - } + rc := InitGaiaXRegistryVerifier(server.URL) + got, err := rc.GetComplianceIssuers() if (err != nil) != tt.wantErr { t.Errorf("GaiaXRegistryClient.GetComplianceIssuers() error = %v, wantErr %v", err, tt.wantErr) diff --git a/verifier/verifiable_credential.go b/verifier/verifiable_credential.go index 79684c30..7f736863 100644 --- a/verifier/verifiable_credential.go +++ b/verifier/verifiable_credential.go @@ -4,11 +4,16 @@ import "github.com/mitchellh/mapstructure" // Subset of the structure of a Verifiable Credential type VerifiableCredential struct { - Id string `mapstructure:"id"` - Types []string `mapstructure:"type"` - Issuer string `mapstructure:"issuer"` - CredentialSubject CredentialSubject `mapstructure:"credentialSubject"` - Raw map[string]interface{} // The unaltered complete credential + MappableVerifiableCredential + raw map[string]interface{} // The unaltered complete credential +} + +// TODO Issue fix to mapstructure to enable combination of "DecoderConfig.ErrorUnset" and an unmapped/untagged field +type MappableVerifiableCredential struct { + Id string `mapstructure:"id"` + Types []string `mapstructure:"type"` + Issuer string `mapstructure:"issuer"` + CredentialSubject CredentialSubject `mapstructure:"credentialSubject"` } // Subset of the structure of a CredentialSubject inside a Verifiable Credential @@ -22,7 +27,7 @@ func (vc VerifiableCredential) GetCredentialType() string { } func (vc VerifiableCredential) GetRawData() map[string]interface{} { - return vc.Raw + return vc.raw } func (vc VerifiableCredential) GetIssuer() string { @@ -30,10 +35,13 @@ func (vc VerifiableCredential) GetIssuer() string { } func MapVerifiableCredential(raw map[string]interface{}) (VerifiableCredential, error) { - var data VerifiableCredential + var data MappableVerifiableCredential + config := &mapstructure.DecoderConfig{ - ErrorUnused: false, - Result: &data, + ErrorUnused: false, + Result: &data, + ErrorUnset: true, + IgnoreUntaggedFields: true, } decoder, err := mapstructure.NewDecoder(config) if err != nil { @@ -42,6 +50,5 @@ func MapVerifiableCredential(raw map[string]interface{}) (VerifiableCredential, if err := decoder.Decode(raw); err != nil { return VerifiableCredential{}, err } - data.Raw = raw - return data, nil + return VerifiableCredential{data, raw}, nil } diff --git a/verifier/verifiable_credential_test.go b/verifier/verifiable_credential_test.go index 2c999766..6135f0c3 100644 --- a/verifier/verifiable_credential_test.go +++ b/verifier/verifiable_credential_test.go @@ -40,20 +40,28 @@ func TestMapVerifiableCredential(t *testing.T) { "ValidCertificate", args{exampleCredential}, VerifiableCredential{ - Id: "https://happypets.fiware.io/credential/25159389-8dd17b796ac0", - Types: []string{ - "VerifiableCredential", - "CustomerCredential", - }, - Issuer: "did:key:verifier", - Raw: exampleCredential, - CredentialSubject: CredentialSubject{ - Id: "someId", - SubjectType: "gx:compliance", + MappableVerifiableCredential{ + Id: "https://happypets.fiware.io/credential/25159389-8dd17b796ac0", + Types: []string{ + "VerifiableCredential", + "CustomerCredential", + }, + Issuer: "did:key:verifier", + CredentialSubject: CredentialSubject{ + Id: "someId", + SubjectType: "gx:compliance", + }, }, + exampleCredential, }, false, }, + { + "InvalidCertificate", + args{map[string]interface{}{"someThing": "else"}}, + VerifiableCredential{}, + true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/verifier/verifier.go b/verifier/verifier.go index 41557323..63b023e8 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -312,26 +312,42 @@ func (v *CredentialVerifier) AuthenticationResponse(state string, verifiableCred } loginSession := loginSessionInterface.(loginSession) + mappedCredentials := []VerifiableCredential{} for _, vc := range verifiableCredentials { mappedCredential, err := MapVerifiableCredential(vc) if err != nil { logging.Log().Warnf("Failed to map credential %s. Err: %v", logging.PrettyPrintObject(vc), err) return sameDevice, err } + mappedCredentials = append(mappedCredentials, mappedCredential) + } + + for _, mappedCredential := range mappedCredentials { //FIXME make it an error if no policy was checked at all( possible misconfiguration) for _, verificationService := range v.verificationServices { result, err := verificationService.VerifyVC(mappedCredential) if err != nil { - logging.Log().Warnf("Failed to verify credential %s. Err: %v", logging.PrettyPrintObject(vc), err) + logging.Log().Warnf("Failed to verify credential %s. Err: %v", logging.PrettyPrintObject(mappedCredential), err) return sameDevice, err } if !result { - logging.Log().Infof("VC %s is not valid.", logging.PrettyPrintObject(vc)) + logging.Log().Infof("VC %s is not valid.", logging.PrettyPrintObject(mappedCredential)) return sameDevice, ErrorInvalidVC } } } + // TODO extract into separate policy + result, err := verifyChain(mappedCredentials) + if err != nil { + logging.Log().Warnf("Failed to verify credentials %s. Err: %v", logging.PrettyPrintObject(mappedCredentials), err) + return sameDevice, err + } + if !result { + logging.Log().Infof("VCs %s have invalid trust chain.", logging.PrettyPrintObject(mappedCredentials)) + return sameDevice, ErrorInvalidVC + } + // we ignore the error here, since the only consequence is that sub will be empty. hostname, _ := getHostName(loginSession.callback) @@ -357,6 +373,40 @@ func (v *CredentialVerifier) AuthenticationResponse(state string, verifiableCred } } +// TODO Use more generic approach to validate that every credential is issued by a party that we trust +func verifyChain(vcs []VerifiableCredential) (bool, error) { + if len(vcs) != 3 { + // TODO Simplification to be removed/replaced + return true, nil + } + + var legalEntity VerifiableCredential + var naturalEntity VerifiableCredential + var compliance VerifiableCredential + + for _, vc := range vcs { + if vc.GetCredentialType() == "gx:LegalParticipant" { + legalEntity = vc + } + if vc.GetCredentialType() == "gx:compliance" { + compliance = vc + } + if vc.GetCredentialType() == "gx:NaturalParticipant" { + naturalEntity = vc + } + } + + // Make sure that the compliance credential is issued for the given credential + if legalEntity.Id != compliance.CredentialSubject.Id { + return false, fmt.Errorf("Compliance credential was not issued for the presented legal entity. Compliance VC subject id %s, legal VC id %s", compliance.CredentialSubject.Id, legalEntity.Id) + } + // Natural participientVC must be issued by the legal participient VC + if legalEntity.CredentialSubject.Id != naturalEntity.Issuer { + return false, fmt.Errorf("Compliance credential was not issued for the presented legal entity. Compliance VC id %s, natural VC issuer %s", compliance.Id, naturalEntity.Issuer) + } + return true, nil +} + // initializes the cross-device siop flow func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback string, sessionId string) (authenticationRequest string, err error) { state := v.nonceGenerator.GenerateNonce() @@ -382,7 +432,11 @@ func (v *CredentialVerifier) generateJWT(verifiableCredentials []map[string]inte if v.scope != "" { jwtBuilder.Claim("scope", v.scope) } - jwtBuilder.Claim("verifiableCredential", verifiableCredentials[0]) + if len(verifiableCredentials) > 1 { + jwtBuilder.Claim("verifiablePresentation", verifiableCredentials) + } else { + jwtBuilder.Claim("verifiableCredential", verifiableCredentials[0]) + } token, err := jwtBuilder.Build() if err != nil { diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 4c5aec98..104de224 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -386,6 +386,7 @@ func getVC(id string) map[string]interface{} { "expirationDate": "2032-11-23T15:23:13Z", "credentialSubject": map[string]interface{}{ "id": id, + "type": "gx:NaturalParticipent", "target": "did:ebsi:packetdelivery", }, } From 58ff09848e42779d6f756657bea030e86a909b2d Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Mon, 24 Apr 2023 14:12:08 +0200 Subject: [PATCH 07/12] rebase --- server.yaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/server.yaml b/server.yaml index de17d24e..e0b5206d 100644 --- a/server.yaml +++ b/server.yaml @@ -13,14 +13,12 @@ verifier: IssuedDateBeforePolicy: {} ValidFromBeforePolicy: {} ExpirationDateAfterPolicy: {} - TrustedIssuerRegistryPolicy: + EbsiTrustedIssuerRegistryPolicy: registryAddress: https://tir.de + issuerType: Undefined credentialTypeSpecific: "gx:compliance": GaiaXComplianceIssuer: registryAddress: https://registry.gaia-x.fiware.dev/development/api/complianceIssuers ssiKit: - auditorURL: http://my-auditor - - - \ No newline at end of file + auditorURL: http://my-auditor \ No newline at end of file From f79d7ed6bb74fd6207c8e1931e9c85f1416e8cd2 Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Mon, 24 Apr 2023 15:03:26 +0200 Subject: [PATCH 08/12] quickfix array of subjects --- verifier/verifiable_credential.go | 18 ++++++++++- verifier/verifiable_credential_test.go | 41 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/verifier/verifiable_credential.go b/verifier/verifiable_credential.go index 7f736863..41919d41 100644 --- a/verifier/verifiable_credential.go +++ b/verifier/verifiable_credential.go @@ -1,6 +1,10 @@ package verifier -import "github.com/mitchellh/mapstructure" +import ( + "reflect" + + "github.com/mitchellh/mapstructure" +) // Subset of the structure of a Verifiable Credential type VerifiableCredential struct { @@ -37,11 +41,23 @@ func (vc VerifiableCredential) GetIssuer() string { func MapVerifiableCredential(raw map[string]interface{}) (VerifiableCredential, error) { var data MappableVerifiableCredential + credentialSubjectArrayDecoder := func(from, to reflect.Type, data interface{}) (interface{}, error) { + if to != reflect.TypeOf((*CredentialSubject)(nil)).Elem() { + return data, nil + } + if reflect.TypeOf(data).Kind() != reflect.Slice { + return data, nil + } + + return data.([]map[string]interface{})[0], nil + } + config := &mapstructure.DecoderConfig{ ErrorUnused: false, Result: &data, ErrorUnset: true, IgnoreUntaggedFields: true, + DecodeHook: credentialSubjectArrayDecoder, } decoder, err := mapstructure.NewDecoder(config) if err != nil { diff --git a/verifier/verifiable_credential_test.go b/verifier/verifiable_credential_test.go index 6135f0c3..49179102 100644 --- a/verifier/verifiable_credential_test.go +++ b/verifier/verifiable_credential_test.go @@ -25,6 +25,27 @@ var exampleCredential = map[string]interface{}{ "type": "gx:compliance", }, } +var exampleCredentialArraySubject = map[string]interface{}{ + "@context": []string{ + "https://www.w3.org/2018/credentials/v1", + "https://happypets.fiware.io/2022/credentials/employee/v1", + }, + "id": "https://happypets.fiware.io/credential/25159389-8dd17b796ac0", + "type": []string{ + "VerifiableCredential", + "CustomerCredential", + }, + "issuer": "did:key:verifier", + "issuanceDate": "2022-11-23T15:23:13Z", + "validFrom": "2022-11-23T15:23:13Z", + "expirationDate": "2032-11-23T15:23:13Z", + "credentialSubject": []map[string]interface{}{{ + "id": "someId", + "target": "did:ebsi:packetdelivery", + "type": "gx:compliance", + }, + }, +} func TestMapVerifiableCredential(t *testing.T) { type args struct { @@ -56,6 +77,26 @@ func TestMapVerifiableCredential(t *testing.T) { }, false, }, + { + "ValidCertificateArraySubject", + args{exampleCredentialArraySubject}, + VerifiableCredential{ + MappableVerifiableCredential{ + Id: "https://happypets.fiware.io/credential/25159389-8dd17b796ac0", + Types: []string{ + "VerifiableCredential", + "CustomerCredential", + }, + Issuer: "did:key:verifier", + CredentialSubject: CredentialSubject{ + Id: "someId", + SubjectType: "gx:compliance", + }, + }, + exampleCredentialArraySubject, + }, + false, + }, { "InvalidCertificate", args{map[string]interface{}{"someThing": "else"}}, From 1ab1143676b8b963358289e6f5b6086d7635c880 Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Mon, 24 Apr 2023 15:15:44 +0200 Subject: [PATCH 09/12] fix test --- verifier/verifiable_credential.go | 2 +- verifier/verifiable_credential_test.go | 48 +++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/verifier/verifiable_credential.go b/verifier/verifiable_credential.go index 41919d41..374afc95 100644 --- a/verifier/verifiable_credential.go +++ b/verifier/verifiable_credential.go @@ -49,7 +49,7 @@ func MapVerifiableCredential(raw map[string]interface{}) (VerifiableCredential, return data, nil } - return data.([]map[string]interface{})[0], nil + return data.([]interface{})[0], nil } config := &mapstructure.DecoderConfig{ diff --git a/verifier/verifiable_credential_test.go b/verifier/verifiable_credential_test.go index 49179102..31d611cf 100644 --- a/verifier/verifiable_credential_test.go +++ b/verifier/verifiable_credential_test.go @@ -3,6 +3,8 @@ package verifier import ( "reflect" "testing" + + json2 "encoding/json" ) var exampleCredential = map[string]interface{}{ @@ -39,7 +41,7 @@ var exampleCredentialArraySubject = map[string]interface{}{ "issuanceDate": "2022-11-23T15:23:13Z", "validFrom": "2022-11-23T15:23:13Z", "expirationDate": "2032-11-23T15:23:13Z", - "credentialSubject": []map[string]interface{}{{ + "credentialSubject": []interface{}{map[string]interface{}{ "id": "someId", "target": "did:ebsi:packetdelivery", "type": "gx:compliance", @@ -47,6 +49,50 @@ var exampleCredentialArraySubject = map[string]interface{}{ }, } +func getVCFromJson() map[string]interface{} { + jsonStr := `{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "http://gx-registry-development:3000/development/api/trusted-shape-registry/v1/shapes/jsonld/trustframework#" + ], + "type": [ + "VerifiableCredential" + ], + "id": "https://storage.gaia-x.eu/credential-offers/b3e0a068-4bf8-4796-932e-2fa83043e203", + "issuer": "did:web:compliance.lab.gaia-x.eu:development", + "issuanceDate": "2023-04-24T13:09:41.885Z", + "expirationDate": "2023-07-23T13:09:41.885Z", + "credentialSubject": [ + { + "type": "gx:compliance", + "id": "did:web:raw.githubusercontent.com:egavard:payload-sign:master", + "integrity": "sha256-9fc56e0099742e57d467156c4526ba723981b2e91eb0ccf6b725ec65b968fcc8" + } + ], + "proof": { + "type": "JsonWebSignature2020", + "created": "2023-04-24T13:09:42.564Z", + "proofPurpose": "assertionMethod", + "jws": "eyJhbGciOiJQUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..FqKjKBWDrfYnFxbZ1TbJYBir0mwy_dya0yO2EGATlHJHD8m9G6fuiKXGYiCnEwGbe81jGKYWzUuq43if8klpszJ8EXmqIVMBHBJWymIrHD9bD4-P4uhx6TqZdkRXvvLUUkjpvOc_JdrntOCIpxNN68yV7NqKHKdRV_rbp4wIstdbCuyZdlAuGHuIow9iEOIfS4-9hdunDh-LBYcI7Mb6NePaKi48tJmO2HDiN3ysYJ15yQ-Pb5dfJtaQCq2o2QJ9ayu2kV4SQHoobMJrBESskQLdLGW_LIPFMRMiRQhE4vYytm61nuFcCTNc9ZHNVzWwOupSpYW3w0YjXQ_xZxH0TQ", + "verificationMethod": "did:web:compliance.lab.gaia-x.eu:development" + } + }` + x := map[string]interface{}{} + + json2.Unmarshal([]byte(jsonStr), &x) + return x +} + +func TestCompliance(t *testing.T) { + _, err := MapVerifiableCredential(getVCFromJson()) + + if err != nil { + t.Errorf("MapVerifiableCredential() error = %v", err) + return + } + +} + func TestMapVerifiableCredential(t *testing.T) { type args struct { raw map[string]interface{} From fc58de3ea58fcd0f81160d1ac0611fa239a9ddef Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Mon, 24 Apr 2023 16:10:14 +0200 Subject: [PATCH 10/12] Update verifier.go --- verifier/verifier.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verifier/verifier.go b/verifier/verifier.go index 63b023e8..b8281024 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -397,7 +397,7 @@ func verifyChain(vcs []VerifiableCredential) (bool, error) { } // Make sure that the compliance credential is issued for the given credential - if legalEntity.Id != compliance.CredentialSubject.Id { + if legalEntity.CredentialSubject.Id != compliance.CredentialSubject.Id { return false, fmt.Errorf("Compliance credential was not issued for the presented legal entity. Compliance VC subject id %s, legal VC id %s", compliance.CredentialSubject.Id, legalEntity.Id) } // Natural participientVC must be issued by the legal participient VC From 273cd878824e480c4780dada927c707d9b0378ab Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Tue, 25 Apr 2023 12:54:48 +0200 Subject: [PATCH 11/12] Increase test coverage --- verifier/gaiax_test.go | 89 ++++++++++++++++++++++++++ verifier/verifiable_credential.go | 10 ++- verifier/verifiable_credential_test.go | 9 +-- 3 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 verifier/gaiax_test.go diff --git a/verifier/gaiax_test.go b/verifier/gaiax_test.go new file mode 100644 index 00000000..cc482efb --- /dev/null +++ b/verifier/gaiax_test.go @@ -0,0 +1,89 @@ +package verifier + +import ( + "errors" + "testing" + + configModel "github.com/fiware/VCVerifier/config" + "github.com/fiware/VCVerifier/gaiax" +) + +type mockRegistryClient struct { + returnValues []string + err error +} + +func (mrc *mockRegistryClient) GetComplianceIssuers() ([]string, error) { + return mrc.returnValues, mrc.err +} + +func createConfig(defaultsEnabled, specific bool) *configModel.Verifier { + conf := configModel.Verifier{PolicyConfig: configModel.Policies{configModel.PolicyMap{}, make(map[string]configModel.PolicyMap)}} + if defaultsEnabled { + conf.PolicyConfig.DefaultPolicies[gaiaxCompliancePolicy] = configModel.PolicyConfigParameters{"registryAddress": "test.com"} + } + if specific { + conf.PolicyConfig.CredentialTypeSpecificPolicies["gx:compliance"] = configModel.PolicyMap{gaiaxCompliancePolicy: configModel.PolicyConfigParameters{"registryAddress": "test.com"}} + } + return &conf +} + +func TestGaiaXRegistryVerifier_VerifyVC(t *testing.T) { + type fields struct { + verifierConfig *configModel.Verifier + gaiaxRegistryClient gaiax.RegistryClient + } + tests := []struct { + name string + fields fields + verifiableCredential VerifiableCredential + wantResult bool + wantErr bool + }{ + { + "HappyPath", + fields{ + verifierConfig: createConfig(true, false), + gaiaxRegistryClient: &mockRegistryClient{[]string{"someDid"}, nil}, + }, + VerifiableCredential{MappableVerifiableCredential{Issuer: "someDid"}, nil}, + true, + false, + }, + { + "IssuerUnknown", + fields{ + verifierConfig: createConfig(true, false), + gaiaxRegistryClient: &mockRegistryClient{[]string{"someDid"}, nil}, + }, + VerifiableCredential{MappableVerifiableCredential{Issuer: "someUnknownDid"}, nil}, + false, + false, + }, + { + "RegistryIssue", + fields{ + verifierConfig: createConfig(true, false), + gaiaxRegistryClient: &mockRegistryClient{[]string{}, errors.New("Registry failed")}, + }, + VerifiableCredential{MappableVerifiableCredential{Issuer: "someDid"}, nil}, + false, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := InitGaiaXRegistryVerifier(tt.fields.verifierConfig) + v.gaiaxRegistryClient = tt.fields.gaiaxRegistryClient + + gotResult, err := v.VerifyVC(tt.verifiableCredential) + if (err != nil) != tt.wantErr { + t.Errorf("GaiaXRegistryVerifier.VerifyVC() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotResult != tt.wantResult { + t.Errorf("GaiaXRegistryVerifier.VerifyVC() = %v, want %v", gotResult, tt.wantResult) + } + }) + } +} diff --git a/verifier/verifiable_credential.go b/verifier/verifiable_credential.go index 374afc95..72801757 100644 --- a/verifier/verifiable_credential.go +++ b/verifier/verifiable_credential.go @@ -3,6 +3,7 @@ package verifier import ( "reflect" + logging "github.com/fiware/VCVerifier/logging" "github.com/mitchellh/mapstructure" ) @@ -48,8 +49,13 @@ func MapVerifiableCredential(raw map[string]interface{}) (VerifiableCredential, if reflect.TypeOf(data).Kind() != reflect.Slice { return data, nil } - - return data.([]interface{})[0], nil + vcArray := data.([]interface{}) + if len(vcArray) > 0{ + logging.Log().Warn("Found more than one credential subject. Will only use/validate first one.") + return vcArray[0], nil + }else{ + return []interface{}{},nil + } } config := &mapstructure.DecoderConfig{ diff --git a/verifier/verifiable_credential_test.go b/verifier/verifiable_credential_test.go index 31d611cf..6ea6b131 100644 --- a/verifier/verifiable_credential_test.go +++ b/verifier/verifiable_credential_test.go @@ -49,7 +49,8 @@ var exampleCredentialArraySubject = map[string]interface{}{ }, } -func getVCFromJson() map[string]interface{} { +// Uses generated credential from https://compliance.lab.gaia-x.eu/development/docs/#/credential-offer/CommonController_issueVC +func getComplianceVCFromJson() map[string]interface{} { jsonStr := `{ "@context": [ "https://www.w3.org/2018/credentials/v1", @@ -83,14 +84,14 @@ func getVCFromJson() map[string]interface{} { return x } -func TestCompliance(t *testing.T) { - _, err := MapVerifiableCredential(getVCFromJson()) + +func TestActualComplianceCredential(t *testing.T) { + _, err := MapVerifiableCredential(getComplianceVCFromJson()) if err != nil { t.Errorf("MapVerifiableCredential() error = %v", err) return } - } func TestMapVerifiableCredential(t *testing.T) { From f5f293c05d786ee9465e5874af539a8b7be68af4 Mon Sep 17 00:00:00 2001 From: Tim Smyth Date: Tue, 25 Apr 2023 13:39:46 +0200 Subject: [PATCH 12/12] docu --- verifier/verifier.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/verifier/verifier.go b/verifier/verifier.go index b8281024..4296a0c0 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -398,11 +398,11 @@ func verifyChain(vcs []VerifiableCredential) (bool, error) { // Make sure that the compliance credential is issued for the given credential if legalEntity.CredentialSubject.Id != compliance.CredentialSubject.Id { - return false, fmt.Errorf("Compliance credential was not issued for the presented legal entity. Compliance VC subject id %s, legal VC id %s", compliance.CredentialSubject.Id, legalEntity.Id) + return false, fmt.Errorf("compliance credential was not issued for the presented legal entity. Compliance VC subject id %s, legal VC id %s", compliance.CredentialSubject.Id, legalEntity.Id) } // Natural participientVC must be issued by the legal participient VC if legalEntity.CredentialSubject.Id != naturalEntity.Issuer { - return false, fmt.Errorf("Compliance credential was not issued for the presented legal entity. Compliance VC id %s, natural VC issuer %s", compliance.Id, naturalEntity.Issuer) + return false, fmt.Errorf("natural participent credential was not issued by the presented legal entity. Legal Participant VC id %s, natural VC issuer %s", legalEntity.CredentialSubject.Id, naturalEntity.Issuer) } return true, nil }