diff --git a/config/config.go b/config/config.go index 8bc59ee9..2ba40eb5 100644 --- a/config/config.go +++ b/config/config.go @@ -48,4 +48,17 @@ 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 { + // 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 + +type PolicyConfigParameters map[string]interface{} diff --git a/config/data/config_test.yaml b/config/data/config_test.yaml index ef279914..336b5423 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" + credentialTypeSpecific: + "gx:compliance": + ValidFromBeforePolicy: {} ssiKit: auditorURL: http://waltid:7003 \ No newline at end of file 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/gaiax/registry.go b/gaiax/registry.go new file mode 100644 index 00000000..104ffe76 --- /dev/null +++ b/gaiax/registry.go @@ -0,0 +1,56 @@ +package gaiax + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/fiware/VCVerifier/logging" +) + +var ErrorRegistry = errors.New("gaiax_registry_failed_to_answer_properly") + +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{}, ErrorRegistry + } + if response.StatusCode != 200 { + 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().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().Warnf("Was not able to decode the issuers list. Was %s", logging.PrettyPrintObject(response)) + return []string{}, err + } + 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..f6645720 --- /dev/null +++ b/gaiax/registry_test.go @@ -0,0 +1,79 @@ +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 := InitGaiaXRegistryVerifier(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/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/server.yaml b/server.yaml index a78dc2f4..e0b5206d 100644 --- a/server.yaml +++ b/server.yaml @@ -7,6 +7,18 @@ logging: verifier: tirAddress: https://tir.de did: did:key:myverifier - + policies: + default: + SignaturePolicy: {} + IssuedDateBeforePolicy: {} + ValidFromBeforePolicy: {} + ExpirationDateAfterPolicy: {} + 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 diff --git a/ssikit/ssikit.go b/ssikit/ssikit.go index c7b608c9..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,12 +32,21 @@ type Policy struct { Argument *TirArgument `json:"argument,omitempty"` } -// TrustedIssuerRegistry Policy Argument - has to be provided to waltId -type TirArgument struct { - RegistryAddress string `json:"registryAddress"` - IssuerType string `json:"issuerType"` +// 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{} + for name, value := range arguments { + (*policy.Argument)[name] = fmt.Sprintf("%v", value) + } + } + return } +// TrustedIssuerRegistry Policy Argument - has to be provided to waltId +type TirArgument map[string]string + // request structure for validating VCs at waltId type verificationRequest struct { Policies []Policy `json:"policies"` 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/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/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/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 new file mode 100644 index 00000000..72801757 --- /dev/null +++ b/verifier/verifiable_credential.go @@ -0,0 +1,76 @@ +package verifier + +import ( + "reflect" + + logging "github.com/fiware/VCVerifier/logging" + "github.com/mitchellh/mapstructure" +) + +// Subset of the structure of a Verifiable Credential +type VerifiableCredential struct { + 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 +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 (vc VerifiableCredential) GetIssuer() string { + return vc.Issuer +} + +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 + } + 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{ + ErrorUnused: false, + Result: &data, + ErrorUnset: true, + IgnoreUntaggedFields: true, + DecodeHook: credentialSubjectArrayDecoder, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return VerifiableCredential{}, err + } + if err := decoder.Decode(raw); err != nil { + return VerifiableCredential{}, err + } + return VerifiableCredential{data, raw}, nil +} diff --git a/verifier/verifiable_credential_test.go b/verifier/verifiable_credential_test.go new file mode 100644 index 00000000..6ea6b131 --- /dev/null +++ b/verifier/verifiable_credential_test.go @@ -0,0 +1,166 @@ +package verifier + +import ( + "reflect" + "testing" + + json2 "encoding/json" +) + +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", + }, +} +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": []interface{}{map[string]interface{}{ + "id": "someId", + "target": "did:ebsi:packetdelivery", + "type": "gx:compliance", + }, + }, +} + +// 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", + "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 TestActualComplianceCredential(t *testing.T) { + _, err := MapVerifiableCredential(getComplianceVCFromJson()) + + if err != nil { + t.Errorf("MapVerifiableCredential() error = %v", err) + return + } +} + +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{ + 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, + }, + { + "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"}}, + VerifiableCredential{}, + true, + }, + } + 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 125d4fdb..4296a0c0 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -48,20 +48,21 @@ type Verifier interface { AuthenticationResponse(state string, verifiableCredentials []map[string]interface{}, holder string) (sameDevice SameDeviceResponse, err error) } -// implementation of the verifier, using waltId ssikit as a validation backend. -type SsiKitVerifier struct { +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) +} + +// 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 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 +73,8 @@ type SsiKitVerifier struct { clock Clock // provides the capabilities to signt the jwt tokenSigner TokenSigner + // Verification services to be used on the credentials + verificationServices []ExternalVerificationService } // allow singleton access to the verifier @@ -171,13 +174,12 @@ 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 } + externalGaiaXVerifier := InitGaiaXRegistryVerifier(verifierConfig) key, err := initPrivateKey() if err != nil { @@ -185,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, policies, key, ssiKitClient, sessionCache, tokenCache, &randomGenerator{}, realClock{}, jwtTokenSigner{}} + 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 @@ -194,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) @@ -213,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) @@ -222,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() @@ -242,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 @@ -274,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) @@ -285,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) @@ -296,18 +312,42 @@ func (v *SsiKitVerifier) AuthenticationResponse(state string, verifiableCredenti } loginSession := loginSessionInterface.(loginSession) + mappedCredentials := []VerifiableCredential{} 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 + 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(mappedCredential), err) + return sameDevice, err + } + if !result { + 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) @@ -333,8 +373,42 @@ func (v *SsiKitVerifier) AuthenticationResponse(state string, verifiableCredenti } } +// 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.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 + if legalEntity.CredentialSubject.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 +} + // 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} @@ -352,13 +426,17 @@ 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 != "" { 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 { @@ -370,7 +448,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 21bbcb29..104de224 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,13 +212,29 @@ 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) } } +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 := 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 { @@ -370,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", }, } @@ -425,7 +442,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() @@ -493,7 +510,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 {