From 7a53e93c91045d6068f8e5e785d68166b1b4553c Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Thu, 25 May 2023 16:01:25 +0200 Subject: [PATCH 1/8] implement tir connection --- config/config.go | 22 +++++- config/data/config_test.yaml | 22 +++++- config/provider_test.go | 14 ++++ gaiax/registry.go | 2 +- gaiax/registry_test.go | 2 +- go.mod | 2 + go.sum | 5 ++ main.go | 2 +- tir/tirClient.go | 136 +++++++++++++++++++++++++++++++++ verifier/cache.go | 9 +++ verifier/credentialsConfig.go | 82 ++++++++++++++++++++ verifier/gaiax.go | 10 +-- verifier/gaiax_test.go | 10 +-- verifier/ssikit.go | 12 ++- verifier/trustedissuer.go | 17 +++++ verifier/trustedparticipant.go | 24 ++++++ verifier/verifier.go | 48 ++++++++---- verifier/verifier_test.go | 6 +- 18 files changed, 386 insertions(+), 39 deletions(-) create mode 100644 tir/tirClient.go create mode 100644 verifier/cache.go create mode 100644 verifier/credentialsConfig.go create mode 100644 verifier/trustedissuer.go create mode 100644 verifier/trustedparticipant.go diff --git a/config/config.go b/config/config.go index 2ba40eb5..4117c5bd 100644 --- a/config/config.go +++ b/config/config.go @@ -4,10 +4,11 @@ package config // general structure of the configuration file type Configuration struct { - Server Server `mapstructure:"server"` - Verifier Verifier `mapstructure:"verifier"` - SSIKit SSIKit `mapstructure:"ssiKit"` - Logging Logging `mapstructure:"logging"` + Server Server `mapstructure:"server"` + Verifier Verifier `mapstructure:"verifier"` + SSIKit SSIKit `mapstructure:"ssiKit"` + Logging Logging `mapstructure:"logging"` + ConfigRepo ConfigRepo `mapstructure:"configRepo"` } // configuration to be used by the ssiKit configuration @@ -59,6 +60,19 @@ type Policies struct { CredentialTypeSpecificPolicies map[string]PolicyMap `mapstructure:"credentialTypeSpecific"` } +type ConfigRepo struct { + // url of the configuration service to be used + ConfigEndpoint string `mapstructure:"configEndpoint"` + // statically configured services with their trust anchors and scopes. + Services map[string]Service `mapstructure:"services"` +} + type PolicyMap map[string]PolicyConfigParameters type PolicyConfigParameters map[string]interface{} + +type Service struct { + Scope []string `mapstructure:"scope"` + TrustedParticipants map[string][]string `mapstructure:"trustedParticipants"` + TrustedIssuers map[string][]string `mapstructure:"trustedIssuers"` +} diff --git a/config/data/config_test.yaml b/config/data/config_test.yaml index 336b5423..12c65598 100644 --- a/config/data/config_test.yaml +++ b/config/data/config_test.yaml @@ -21,5 +21,25 @@ verifier: credentialTypeSpecific: "gx:compliance": ValidFromBeforePolicy: {} + ssiKit: - auditorURL: http://waltid:7003 \ No newline at end of file + auditorURL: http://waltid:7003 + +configRepo: + services: + testService: + scope: + - VerifiableCredential + - CustomerCredential + trustedParticipants: + VerifiableCredential: + - https://tir-pdc.gaia-x.fiware.dev + CustomerCredential: + - https://tir-pdc.gaia-x.fiware.dev + trustedIssuers: + VerifiableCredential: + - https://tir-pdc.gaia-x.fiware.dev + CustomerCredential: + - https://tir-pdc.gaia-x.fiware.dev + + diff --git a/config/provider_test.go b/config/provider_test.go index 35c4efce..9b5cc162 100644 --- a/config/provider_test.go +++ b/config/provider_test.go @@ -53,6 +53,20 @@ func Test_ReadConfig(t *testing.T) { LogRequests: true, PathsToSkip: []string{"/health"}, }, + ConfigRepo: ConfigRepo{ + ConfigEndpoint: "", + Services: map[string]Service{ + "testService": { + Scope: []string{"VerifiableCredential", "CustomerCredential"}, + TrustedParticipants: map[string][]string{ + "VerifiableCredential": {"https://tir-pdc.gaia-x.fiware.dev"}, + "CustomerCredential": {"https://tir-pdc.gaia-x.fiware.dev"}, + }, + TrustedIssuers: map[string][]string{ + "VerifiableCredential": {"https://tir-pdc.gaia-x.fiware.dev"}, + "CustomerCredential": {"https://tir-pdc.gaia-x.fiware.dev"}, + }}}, + }, }, false, }, { diff --git a/gaiax/registry.go b/gaiax/registry.go index 104ffe76..a97444d9 100644 --- a/gaiax/registry.go +++ b/gaiax/registry.go @@ -19,7 +19,7 @@ type GaiaXRegistryClient struct { endpoint string } -func InitGaiaXRegistryVerifier(url string) RegistryClient { +func InitGaiaXRegistryVerificationService(url string) RegistryClient { return &GaiaXRegistryClient{url} } diff --git a/gaiax/registry_test.go b/gaiax/registry_test.go index f6645720..a93d2ae9 100644 --- a/gaiax/registry_test.go +++ b/gaiax/registry_test.go @@ -64,7 +64,7 @@ func TestGaiaXRegistryClient_GetComplianceIssuers(t *testing.T) { })) defer server.Close() - rc := InitGaiaXRegistryVerifier(server.URL) + rc := InitGaiaXRegistryVerificationService(server.URL) got, err := rc.GetComplianceIssuers() if (err != nil) != tt.wantErr { diff --git a/go.mod b/go.mod index a6fad345..1d380a5e 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,8 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.2.0 // indirect + github.com/bxcodec/gotcha v1.0.0-beta.8 // indirect + github.com/bxcodec/httpcache v1.0.0-beta.3 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/fatih/color v1.14.1 // indirect diff --git a/go.sum b/go.sum index ef34f83d..ad5cf2a4 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,11 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bxcodec/gotcha v1.0.0-beta.2/go.mod h1:MEL9PRYL9Squu1zxreMIzJU6xtMouPmQybWEtXrL1nk= +github.com/bxcodec/gotcha v1.0.0-beta.8 h1:2yA1RzMHh4KPc2Fou6mQFd5rgrPHI8nj52un2fQyXkU= +github.com/bxcodec/gotcha v1.0.0-beta.8/go.mod h1:UnXdYOHIGqan8v5AACIHh8nLoCBLb5YifKBeGJOTNBg= +github.com/bxcodec/httpcache v1.0.0-beta.3 h1:4h+Yoda40PthVkiFa5hPtCoH46qbDRq/iCT7M5ACoR4= +github.com/bxcodec/httpcache v1.0.0-beta.3/go.mod h1:NdIkH0u1smImRyp35nIGeqG5IFeKbnyyBIv8cEGSXMY= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.8.5 h1:kjX0/vo5acEQ/sinD/18SkA/lDDUk23F0RcaHvI7omc= github.com/bytedance/sonic v1.8.5/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= diff --git a/main.go b/main.go index bdc46b56..dba696ad 100644 --- a/main.go +++ b/main.go @@ -45,7 +45,7 @@ func main() { logger.Errorf("Was not able to get an ssiKit client. Err: %v", err) return } - verifier.InitVerifier(&configuration.Verifier, ssiKitClient) + verifier.InitVerifier(&configuration.Verifier, &configuration.ConfigRepo, ssiKitClient) router := getRouter() diff --git a/tir/tirClient.go b/tir/tirClient.go new file mode 100644 index 00000000..46faa0b7 --- /dev/null +++ b/tir/tirClient.go @@ -0,0 +1,136 @@ +package tir + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + "time" + + "github.com/bxcodec/httpcache" + "github.com/fiware/VCVerifier/logging" +) + +const ISSUERS_PATH = "v4/issuers" + +var ErrorTirNoResponse = errors.New("no_response_from_tir") +var ErrorTirEmptyResponse = errors.New("empty_response_from_tir") + +type HttpClient interface { + Get(url string) (resp *http.Response, err error) +} + +/** +* A client to retrieve infromation from EBSI-compatible TrustedIssuerRegistry APIs. + */ +type TirClient struct { + client HttpClient +} + +/** +* A trusted issuer as defined by EBSI + */ +type TrustedIssuer struct { + Did string `json:"did"` + Attributes []IssuerAttribute `json:"attributes"` +} + +/** +* Attribute of an issuer + */ +type IssuerAttribute struct { + Hash string `json:"hash"` + Body string `json:"body"` + IssuerType string `json:"issuerType"` + Tao string `json:"tao"` + RootTao string `json:"rootTao"` +} + +func NewTirClient() (client TirClient, err error) { + + httpClient := &http.Client{} + _, err = httpcache.NewWithInmemoryCache(httpClient, true, time.Second*60) + if err != nil { + logging.Log().Errorf("Was not able to inject the cach to the client. Err: %v", err) + return + } + return TirClient{httpClient}, err +} + +func (tc TirClient) IsTrustedParticipant(tirEndpoints []string, did string) (trusted bool) { + + for _, tirEndpoint := range tirEndpoints { + logging.Log().Debugf("Check if a participant %s is trusted through %s.", did, tirEndpoint) + if tc.issuerExists(getIssuerUrl(tirEndpoint), did) { + logging.Log().Debugf("Issuer %s is a trusted participant via %s.", did, tirEndpoint) + return true + } + } + return false +} + +func (tc TirClient) GetTrustedIssuer(tirEndpoints []string, did string) (exists bool, trustedIssuer TrustedIssuer, err error) { + for _, tirEndpoint := range tirEndpoints { + resp, err := tc.requestIssuer(tirEndpoint, did) + if err != nil { + logging.Log().Warnf("Was not able to get the issuer %s from %s because of err: %v.", did, tirEndpoint, err) + continue + } + if resp.StatusCode != 200 { + logging.Log().Debugf("Issuer %s is not known at %s.", did, tirEndpoint) + continue + } + trustedIssuer, err := parseTirResponse(*resp) + if err != nil { + logging.Log().Warnf("Was not able to parse the response from tir %s for %s. Err: %v", tirEndpoint, did, err) + continue + } + return true, trustedIssuer, err + } + return false, trustedIssuer, err +} + +func parseTirResponse(resp http.Response) (trustedIssuer TrustedIssuer, err error) { + + if resp.Body == nil { + logging.Log().Info("Received an empty body from the tir.") + return trustedIssuer, ErrorTirEmptyResponse + } + + err = json.NewDecoder(resp.Body).Decode(&trustedIssuer) + if err != nil { + logging.Log().Warn("Was not able to decode the tir-response.") + return trustedIssuer, err + } + return trustedIssuer, err +} + +func (tc TirClient) issuerExists(tirEndpoint string, did string) (trusted bool) { + resp, err := tc.requestIssuer(tirEndpoint, did) + if err != nil { + return false + } + // if a 200 is returned, the issuer exists. We dont have to parse the whole response + return resp.StatusCode == 200 +} + +func (tc TirClient) requestIssuer(tirEndpoint string, did string) (response *http.Response, err error) { + resp, err := tc.client.Get(tirEndpoint + "/" + did) + if err != nil { + logging.Log().Warnf("Was not able to get the issuer %s from %s. Err: %v", did, tirEndpoint, err) + return resp, err + } + if resp == nil { + logging.Log().Warnf("Was not able to get any response for issuer %s from %s.", did, tirEndpoint) + return nil, ErrorTirNoResponse + } + return resp, err +} + +func getIssuerUrl(tirEndpoint string) string { + if strings.HasSuffix(tirEndpoint, "/") { + return tirEndpoint + ISSUERS_PATH + } else { + return tirEndpoint + "/" + ISSUERS_PATH + } +} diff --git a/verifier/cache.go b/verifier/cache.go new file mode 100644 index 00000000..d17e9a1d --- /dev/null +++ b/verifier/cache.go @@ -0,0 +1,9 @@ +package verifier + +import "time" + +type Cache interface { + Add(k string, x interface{}, d time.Duration) error + Get(k string) (interface{}, bool) + Delete(k string) +} diff --git a/verifier/credentialsConfig.go b/verifier/credentialsConfig.go new file mode 100644 index 00000000..7834c9df --- /dev/null +++ b/verifier/credentialsConfig.go @@ -0,0 +1,82 @@ +package verifier + +import ( + "fmt" + "net/url" + "time" + + "github.com/fiware/VCVerifier/config" + "github.com/fiware/VCVerifier/logging" + "github.com/patrickmn/go-cache" +) + +const CACHE_EXPIRY = 30 +const CACHE_KEY_TEMPLATE = "%s-%s" + +/** +* Provides information about credentialTypes associated with services and there trust anchors. + */ +type CredentialsConfig interface { + // should return the list of credentialtypes to be requested via the scope parameter + GetScope(serviceIdentifier string) (credentialTypes []string, err error) + // get (EBSI TrustedIssuersRegistry compliant) endpoints for the given service/credential combination, to check its issued by a trusted participant. + GetTrustedParticipantLists(serviceIdentifier string, credentialType string) (trustedIssuersRegistryUrl []string, err error) + // get (EBSI TrustedIssuersRegistry compliant) endpoints for the given service/credential combination, to check that credentials are issued by trusted issuers + // and that the issuer has permission to issue such claims. + GetTrustedIssuersLists(serviceIdentifier string, credentialType string) (trustedIssuersRegistryUrl []string, err error) +} + +type ServiceBackedCredentialsConfig struct { + configEndpoint *url.URL + scopeCache Cache + trustedParticipantsCache Cache + trustedIssuersCache Cache +} + +func InitServiceBackedCredentialsConfig(repoConfig *config.ConfigRepo) (credentialsConfig CredentialsConfig, err error) { + if repoConfig.ConfigEndpoint == "" { + logging.Log().Warn("No endpoint for the configuration service is configured. Only static configuration will be provided.") + } + serviceUrl, err := url.Parse(repoConfig.ConfigEndpoint) + if err != nil { + logging.Log().Errorf("The service endpoint %s is not a valid url. Err: %v", repoConfig.ConfigEndpoint, err) + return + } + scopeCache := cache.New(time.Duration(CACHE_EXPIRY)*time.Second, time.Duration(2*CACHE_EXPIRY)*time.Second) + trustedParticipantsCache := cache.New(time.Duration(CACHE_EXPIRY)*time.Second, time.Duration(2*CACHE_EXPIRY)*time.Second) + trustedIssuersCache := cache.New(time.Duration(CACHE_EXPIRY)*time.Second, time.Duration(2*CACHE_EXPIRY)*time.Second) + for serviceId, serviceConfig := range repoConfig.Services { + scopeCache.Add(serviceId, serviceConfig.Scope, cache.DefaultExpiration) + for vcType, trustedParticipants := range serviceConfig.TrustedParticipants { + trustedParticipantsCache.Add(fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceId, vcType), trustedParticipants, cache.DefaultExpiration) + } + for vcType, trustedIssuers := range serviceConfig.TrustedIssuers { + trustedIssuersCache.Add(fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceId, vcType), trustedIssuers, cache.DefaultExpiration) + } + } + return ServiceBackedCredentialsConfig{configEndpoint: serviceUrl, scopeCache: scopeCache, trustedParticipantsCache: trustedParticipantsCache, trustedIssuersCache: trustedIssuersCache}, err +} + +func (cc ServiceBackedCredentialsConfig) GetScope(serviceIdentifier string) (credentialTypes []string, err error) { + cacheEntry, hit := cc.scopeCache.Get(serviceIdentifier) + if hit { + return cacheEntry.([]string), nil + } + return []string{}, nil +} + +func (cc ServiceBackedCredentialsConfig) GetTrustedParticipantLists(serviceIdentifier string, credentialType string) (trustedIssuersRegistryUrl []string, err error) { + cacheEntry, hit := cc.trustedParticipantsCache.Get(fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceIdentifier, credentialType)) + if hit { + return cacheEntry.([]string), nil + } + return []string{}, nil +} + +func (cc ServiceBackedCredentialsConfig) GetTrustedIssuersLists(serviceIdentifier string, credentialType string) (trustedIssuersRegistryUrl []string, err error) { + cacheEntry, hit := cc.trustedIssuersCache.Get(fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceIdentifier, credentialType)) + if hit { + return cacheEntry.([]string), nil + } + return []string{}, nil +} diff --git a/verifier/gaiax.go b/verifier/gaiax.go index e7adc7b4..77bf0584 100644 --- a/verifier/gaiax.go +++ b/verifier/gaiax.go @@ -13,16 +13,16 @@ import ( const gaiaxCompliancePolicy = "GaiaXComplianceIssuer" const registryUrlPropertyName = "registryAddress" -type GaiaXRegistryVerifier struct { +type GaiaXRegistryVerificationService struct { validateAll bool credentialTypesToValidate []string // client for gaiax registry connection gaiaxRegistryClient gaiax.RegistryClient } -func InitGaiaXRegistryVerifier(verifierConfig *configModel.Verifier) GaiaXRegistryVerifier { +func InitGaiaXRegistryVerificationService(verifierConfig *configModel.Verifier) GaiaXRegistryVerificationService { var url string - verifier := GaiaXRegistryVerifier{credentialTypesToValidate: []string{}} + verifier := GaiaXRegistryVerificationService{credentialTypesToValidate: []string{}} for policyName, arguments := range verifierConfig.PolicyConfig.DefaultPolicies { if policyName == gaiaxCompliancePolicy { @@ -39,12 +39,12 @@ func InitGaiaXRegistryVerifier(verifierConfig *configModel.Verifier) GaiaXRegist } } if len(url) > 0 { - verifier.gaiaxRegistryClient = gaiax.InitGaiaXRegistryVerifier(url) + verifier.gaiaxRegistryClient = gaiax.InitGaiaXRegistryVerificationService(url) } return verifier } -func (v *GaiaXRegistryVerifier) VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) { +func (v *GaiaXRegistryVerificationService) VerifyVC(verifiableCredential VerifiableCredential, verificationContext VerificationContext) (result bool, err error) { if v.validateAll || slices.Contains(v.credentialTypesToValidate, verifiableCredential.GetCredentialType()) { issuerDids, err := v.gaiaxRegistryClient.GetComplianceIssuers() if err != nil { diff --git a/verifier/gaiax_test.go b/verifier/gaiax_test.go index cc482efb..af7fd775 100644 --- a/verifier/gaiax_test.go +++ b/verifier/gaiax_test.go @@ -28,7 +28,7 @@ func createConfig(defaultsEnabled, specific bool) *configModel.Verifier { return &conf } -func TestGaiaXRegistryVerifier_VerifyVC(t *testing.T) { +func TestGaiaXRegistryVerificationService_VerifyVC(t *testing.T) { type fields struct { verifierConfig *configModel.Verifier gaiaxRegistryClient gaiax.RegistryClient @@ -73,16 +73,16 @@ func TestGaiaXRegistryVerifier_VerifyVC(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - v := InitGaiaXRegistryVerifier(tt.fields.verifierConfig) + v := InitGaiaXRegistryVerificationService(tt.fields.verifierConfig) v.gaiaxRegistryClient = tt.fields.gaiaxRegistryClient - gotResult, err := v.VerifyVC(tt.verifiableCredential) + gotResult, err := v.VerifyVC(tt.verifiableCredential, nil) if (err != nil) != tt.wantErr { - t.Errorf("GaiaXRegistryVerifier.VerifyVC() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GaiaXRegistryVerificationService.VerifyVC() error = %v, wantErr %v", err, tt.wantErr) return } if gotResult != tt.wantResult { - t.Errorf("GaiaXRegistryVerifier.VerifyVC() = %v, want %v", gotResult, tt.wantResult) + t.Errorf("GaiaXRegistryVerificationService.VerifyVC() = %v, want %v", gotResult, tt.wantResult) } }) } diff --git a/verifier/ssikit.go b/verifier/ssikit.go index 19ee88eb..174b78fb 100644 --- a/verifier/ssikit.go +++ b/verifier/ssikit.go @@ -7,7 +7,11 @@ import ( "github.com/fiware/VCVerifier/ssikit" ) -type SsiKitExternalVerifier struct { +/** +* The SsiKit verifier should concentrate on general verification at the credential level(e.g. check signature, expiry etc.). Even thought a TIR policy could +* be configured, its recommended to use the TrustedIssuersRegistryVerifer or TrustedIssuersListVerifier for that purpose. + */ +type SsiKitExternalVerificationService struct { policies PolicyMap credentialSpecificPolicies map[string]PolicyMap // client for connection waltId @@ -21,7 +25,7 @@ func isPolicySupportedBySsiKit(policyName string) bool { return policyName != gaiaxCompliancePolicy } -func InitSsiKitExternalVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIKit) (verifier SsiKitExternalVerifier, err error) { +func InitSsiKitExternalVerificationService(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIKit) (verifier SsiKitExternalVerificationService, err error) { defaultPolicies := PolicyMap{} for policyName, arguments := range verifierConfig.PolicyConfig.DefaultPolicies { if isPolicySupportedBySsiKit(policyName) { @@ -37,10 +41,10 @@ func InitSsiKitExternalVerifier(verifierConfig *configModel.Verifier, ssiKitClie } } } - return SsiKitExternalVerifier{defaultPolicies, credentialSpecificPolicies, ssiKitClient}, nil + return SsiKitExternalVerificationService{defaultPolicies, credentialSpecificPolicies, ssiKitClient}, nil } -func (v *SsiKitExternalVerifier) VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) { +func (v *SsiKitExternalVerificationService) VerifyVC(verifiableCredential VerifiableCredential, verificationContext VerificationContext) (result bool, err error) { usedPolicies := PolicyMap{} for name, policy := range v.policies { usedPolicies[name] = policy diff --git a/verifier/trustedissuer.go b/verifier/trustedissuer.go new file mode 100644 index 00000000..de8f7ea1 --- /dev/null +++ b/verifier/trustedissuer.go @@ -0,0 +1,17 @@ +package verifier + +import ( + tir "github.com/fiware/VCVerifier/tir" +) + +/** +* The trusted participant verification service will validate the entry of a participant within the trusted list. + */ +type TrustedIssuerVerificationService struct { + tirClient tir.TirClient +} + +func (tpvs *TrustedIssuerVerificationService) VerifyVC(verifiableCredential VerifiableCredential, verificationContext VerificationContext) (result bool, err error) { + trustContext := verificationContext.(TrustRegistriesVerificationContext) + tpvs.tirClient.GetTrustedIssuer(trustContext.GetTrustedParticipantLists(), verifiableCredential.Issuer), nil +} diff --git a/verifier/trustedparticipant.go b/verifier/trustedparticipant.go new file mode 100644 index 00000000..0fcdc519 --- /dev/null +++ b/verifier/trustedparticipant.go @@ -0,0 +1,24 @@ +package verifier + +import ( + tir "github.com/fiware/VCVerifier/tir" +) + +/** +* The trusted participant verification service will validate the entry of a participant within the trusted list. + */ +type TrustedParticipantVerificationService struct { + tirClient tir.TirClient +} + +func (tpvs *TrustedParticipantVerificationService) VerifyVC(verifiableCredential VerifiableCredential, verificationContext VerificationContext) (result bool, err error) { + trustContext := verificationContext.(TrustRegistriesVerificationContext) + exist, trustedIssuer, err := tpvs.tirClient.GetTrustedIssuer(trustContext.GetTrustedParticipantLists(), verifiableCredential.Issuer) + if err != nil { + return false, err + } + if !exist { + return false, err + } + trustedIssuer.Attributes +} diff --git a/verifier/verifier.go b/verifier/verifier.go index 4296a0c0..dcbe0c60 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -48,9 +48,9 @@ type Verifier interface { AuthenticationResponse(state string, verifiableCredentials []map[string]interface{}, holder string) (sameDevice SameDeviceResponse, err error) } -type ExternalVerificationService interface { +type VerificationService 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) + VerifyVC(verifiableCredential VerifiableCredential, verificationContext VerificationContext) (result bool, err error) } // implementation of the verifier, using waltId ssikit and gaia-x compliance issuers registry as a validation backends. @@ -73,8 +73,10 @@ type CredentialVerifier struct { clock Clock // provides the capabilities to signt the jwt tokenSigner TokenSigner + // provide the configuration to be used with the credentials + credentialsConfig CredentialsConfig // Verification services to be used on the credentials - verificationServices []ExternalVerificationService + verificationServices []VerificationService } // allow singleton access to the verifier @@ -85,16 +87,25 @@ var httpClient = client.HttpClient() // interfaces and default implementations -type Cache interface { - Add(k string, x interface{}, d time.Duration) error - Get(k string) (interface{}, bool) - Delete(k string) -} - type Clock interface { Now() time.Time } +type VerificationContext interface{} + +type TrustRegistriesVerificationContext struct { + trustedIssuersLists []string + trustedParticipantsRegistries []string +} + +func (trvc TrustRegistriesVerificationContext) GetTrustedIssuersLists() []string { + return trvc.trustedIssuersLists +} + +func (trvc TrustRegistriesVerificationContext) GetTrustedParticipantLists() []string { + return trvc.trustedParticipantsRegistries +} + type realClock struct{} func (realClock) Now() time.Time { @@ -164,7 +175,7 @@ func GetVerifier() Verifier { /** * Initialize the verifier and all its components from the configuration **/ -func InitVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIKit) (err error) { +func InitVerifier(verifierConfig *configModel.Verifier, repoConfig *configModel.ConfigRepo, ssiKitClient ssikit.SSIKit) (err error) { err = verifyConfig(verifierConfig) if err != nil { @@ -174,12 +185,19 @@ 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) - externalSsiKitVerifier, err := InitSsiKitExternalVerifier(verifierConfig, ssiKitClient) + externalSsiKitVerifier, err := InitSsiKitExternalVerificationService(verifierConfig, ssiKitClient) if err != nil { logging.Log().Errorf("Was not able to initiate a external verifier. Err: %v", err) return err } - externalGaiaXVerifier := InitGaiaXRegistryVerifier(verifierConfig) + externalGaiaXVerifier := InitGaiaXRegistryVerificationService(verifierConfig) + + credentialsConfig, err := InitServiceBackedCredentialsConfig(repoConfig) + if err != nil { + logging.Log().Errorf("Was not able to initiate the credentials config. Err: %v", err) + } + + trustedParticipantsVerificationService := TrustedParticipantVerificationService{} key, err := initPrivateKey() if err != nil { @@ -197,9 +215,11 @@ func InitVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIK &randomGenerator{}, realClock{}, jwtTokenSigner{}, - []ExternalVerificationService{ + credentialsConfig, + []VerificationService{ &externalSsiKitVerifier, &externalGaiaXVerifier, + &trustedParticipantsVerificationService, }, } @@ -325,7 +345,7 @@ func (v *CredentialVerifier) AuthenticationResponse(state string, verifiableCred 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) + result, err := verificationService.VerifyVC(mappedCredential, TrustRegistriesVerificationContext{}) if err != nil { logging.Log().Warnf("Failed to verify credential %s. Err: %v", logging.PrettyPrintObject(mappedCredential), err) return sameDevice, err diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 104de224..1b05377e 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -224,7 +224,7 @@ type mockExternalSsiKit struct { verificationError error } -func (msk *mockExternalSsiKit) VerifyVC(verifiableCredential VerifiableCredential) (result bool, err error) { +func (msk *mockExternalSsiKit) VerifyVC(verifiableCredential VerifiableCredential, verificationContext VerificationContext) (result bool, err error) { if msk.verificationError != nil { return result, msk.verificationError } @@ -337,7 +337,7 @@ func TestAuthenticationResponse(t *testing.T) { testKey, _ := jwk.New(ecdsKey) jwk.AssignKeyID(testKey) nonceGenerator := mockNonceGenerator{staticValues: []string{"authCode"}} - verifier := CredentialVerifier{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: []VerificationService{&mockExternalSsiKit{tc.verificationResult, tc.verificationError}}, clock: mockClock{}} sameDeviceResponse, err := verifier.AuthenticationResponse(tc.requestedState, tc.testVC, tc.testHolder) if err != tc.expectedError { @@ -418,7 +418,7 @@ func TestInitVerifier(t *testing.T) { verifier = nil logging.Log().Info("TestInitVerifier +++++++++++++++++ Running test: ", tc.testName) - err := InitVerifier(&tc.testConfig, &mockSsiKit{}) + err := InitVerifier(&tc.testConfig, &configModel.ConfigRepo{}, &mockSsiKit{}) if tc.expectedError != err { t.Errorf("%s - Expected error %v but was %v.", tc.testName, tc.expectedError, err) } From a62a90a4cece7952321974e6cb8895474c25ae6b Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 26 May 2023 14:39:36 +0200 Subject: [PATCH 2/8] connect to tir --- tir/tirClient.go | 38 ++++- verifier/trustedissuer.go | 122 ++++++++++++++- verifier/trustedissuer_test.go | 130 ++++++++++++++++ verifier/trustedparticipant.go | 23 ++- verifier/trustedparticipant_test.go | 55 +++++++ verifier/verifiable_credential.go | 5 +- verifier/verifiable_credential_test.go | 2 + verifier/verifier.go | 13 +- verifier/verifier_test.go | 198 +++++++++++++------------ 9 files changed, 471 insertions(+), 115 deletions(-) create mode 100644 verifier/trustedissuer_test.go create mode 100644 verifier/trustedparticipant_test.go diff --git a/tir/tirClient.go b/tir/tirClient.go index 46faa0b7..abd773f3 100644 --- a/tir/tirClient.go +++ b/tir/tirClient.go @@ -20,10 +20,15 @@ type HttpClient interface { Get(url string) (resp *http.Response, err error) } +type TirClient interface { + IsTrustedParticipant(tirEndpoints []string, did string) (trusted bool) + GetTrustedIssuer(tirEndpoints []string, did string) (exists bool, trustedIssuer TrustedIssuer, err error) +} + /** * A client to retrieve infromation from EBSI-compatible TrustedIssuerRegistry APIs. */ -type TirClient struct { +type TirHttpClient struct { client HttpClient } @@ -46,7 +51,26 @@ type IssuerAttribute struct { RootTao string `json:"rootTao"` } -func NewTirClient() (client TirClient, err error) { +/** +* Configuration of a credentialType, its validity time and the claims allowed to be issued + */ +type Credential struct { + ValidFor TimeRange `json:"validFor"` + CredentialsType string `json:"credentialsType"` + Claims []Claim `json:"claims"` +} + +type TimeRange struct { + From string `json:"from"` + To string `json:"to"` +} + +type Claim struct { + Name string `json:"name"` + AllowedValues []interface{} `json:"allowedValues"` +} + +func NewTirHttpClient() (client TirClient, err error) { httpClient := &http.Client{} _, err = httpcache.NewWithInmemoryCache(httpClient, true, time.Second*60) @@ -54,10 +78,10 @@ func NewTirClient() (client TirClient, err error) { logging.Log().Errorf("Was not able to inject the cach to the client. Err: %v", err) return } - return TirClient{httpClient}, err + return TirHttpClient{httpClient}, err } -func (tc TirClient) IsTrustedParticipant(tirEndpoints []string, did string) (trusted bool) { +func (tc TirHttpClient) IsTrustedParticipant(tirEndpoints []string, did string) (trusted bool) { for _, tirEndpoint := range tirEndpoints { logging.Log().Debugf("Check if a participant %s is trusted through %s.", did, tirEndpoint) @@ -69,7 +93,7 @@ func (tc TirClient) IsTrustedParticipant(tirEndpoints []string, did string) (tru return false } -func (tc TirClient) GetTrustedIssuer(tirEndpoints []string, did string) (exists bool, trustedIssuer TrustedIssuer, err error) { +func (tc TirHttpClient) GetTrustedIssuer(tirEndpoints []string, did string) (exists bool, trustedIssuer TrustedIssuer, err error) { for _, tirEndpoint := range tirEndpoints { resp, err := tc.requestIssuer(tirEndpoint, did) if err != nil { @@ -105,7 +129,7 @@ func parseTirResponse(resp http.Response) (trustedIssuer TrustedIssuer, err erro return trustedIssuer, err } -func (tc TirClient) issuerExists(tirEndpoint string, did string) (trusted bool) { +func (tc TirHttpClient) issuerExists(tirEndpoint string, did string) (trusted bool) { resp, err := tc.requestIssuer(tirEndpoint, did) if err != nil { return false @@ -114,7 +138,7 @@ func (tc TirClient) issuerExists(tirEndpoint string, did string) (trusted bool) return resp.StatusCode == 200 } -func (tc TirClient) requestIssuer(tirEndpoint string, did string) (response *http.Response, err error) { +func (tc TirHttpClient) requestIssuer(tirEndpoint string, did string) (response *http.Response, err error) { resp, err := tc.client.Get(tirEndpoint + "/" + did) if err != nil { logging.Log().Warnf("Was not able to get the issuer %s from %s. Err: %v", did, tirEndpoint, err) diff --git a/verifier/trustedissuer.go b/verifier/trustedissuer.go index de8f7ea1..c8274dcc 100644 --- a/verifier/trustedissuer.go +++ b/verifier/trustedissuer.go @@ -1,7 +1,12 @@ package verifier import ( + "encoding/base64" + "encoding/json" + + "github.com/fiware/VCVerifier/logging" tir "github.com/fiware/VCVerifier/tir" + "golang.org/x/exp/slices" ) /** @@ -12,6 +17,121 @@ type TrustedIssuerVerificationService struct { } func (tpvs *TrustedIssuerVerificationService) VerifyVC(verifiableCredential VerifiableCredential, verificationContext VerificationContext) (result bool, err error) { + defer func() { + if recErr := recover(); recErr != nil { + logging.Log().Warnf("Was not able to convert context. Err: %v", recErr) + err = ErrorCannotConverContext + } + }() trustContext := verificationContext.(TrustRegistriesVerificationContext) - tpvs.tirClient.GetTrustedIssuer(trustContext.GetTrustedParticipantLists(), verifiableCredential.Issuer), nil + exist, trustedIssuer, err := tpvs.tirClient.GetTrustedIssuer(trustContext.GetTrustedParticipantLists(), verifiableCredential.Issuer) + + if err != nil { + return false, err + } + if !exist { + return false, err + } + credentials, err := parseAttributes(trustedIssuer) + if err != nil { + logging.Log().Warnf("Was not able to parse the issuer %s. Err: %v", logging.PrettyPrintObject(trustedIssuer), err) + } + return verifyWithCredentialsConfig(verifiableCredential, credentials) +} + +func verifyWithCredentialsConfig(verifiableCredential VerifiableCredential, credentials []tir.Credential) (result bool, err error) { + + credentialsConfigMap := map[string]tir.Credential{} + allowedTypes := []string{} + // format for better validation + for _, credential := range credentials { + allowedTypes = append(allowedTypes, credential.CredentialsType) + credentialsConfigMap[credential.CredentialsType] = credential + } + // we initalize with true, since there is no case where types can be empty. + var typeAllowed = true + // initalize to true, since everything without a specific rule is considered to be allowed + var subjectAllowed = true + logging.Log().Debugf("Validate that the type %v is allowed by %v.", verifiableCredential.Types, allowedTypes) + // validate that the type(s) is allowed + for _, credentialType := range verifiableCredential.MappableVerifiableCredential.Types { + typeAllowed = typeAllowed && slices.Contains(allowedTypes, credentialType) + subjectAllowed = subjectAllowed && verifyForType(verifiableCredential.MappableVerifiableCredential.CredentialSubject, credentialsConfigMap[credentialType]) + } + if !typeAllowed { + logging.Log().Debugf("Credentials type %s is not allowed.", logging.PrettyPrintObject(verifiableCredential.Types)) + return false, err + } + if !subjectAllowed { + logging.Log().Debugf("The subject contains forbidden claims or values: %s.", logging.PrettyPrintObject(verifiableCredential.CredentialSubject)) + return false, err + } + logging.Log().Debugf("Credential %s is allowed by the config %s.", logging.PrettyPrintObject(verifiableCredential), logging.PrettyPrintObject(credentials)) + return true, err +} + +func verifyForType(subjectToVerfiy CredentialSubject, credentialConfig tir.Credential) (result bool) { + for _, claim := range credentialConfig.Claims { + claimValue, exists := subjectToVerfiy.Claims[claim.Name] + if !exists { + logging.Log().Debugf("Restricted claim %s is not part of the subject %s.", claim.Name, logging.PrettyPrintObject(subjectToVerfiy)) + continue + } + isAllowed := contains(claim.AllowedValues, claimValue) + if !isAllowed { + logging.Log().Debugf("The claim value %s is not allowed by the config %s.", logging.PrettyPrintObject(claimValue), logging.PrettyPrintObject(credentialConfig)) + return false + } + } + logging.Log().Debugf("No forbidden claim found for subject %s. Checked config was %s.", logging.PrettyPrintObject(subjectToVerfiy), logging.PrettyPrintObject(credentialConfig)) + return true +} + +/** +* Check if the given interface is contained. In order to avoid type issues(f.e. if numbers are parsed to different interfaces), +* we marshal and compare the json representation. + */ +func contains(interfaces []interface{}, interfaceToCheck interface{}) bool { + jsonBytesToCheck, err := json.Marshal(interfaceToCheck) + if err != nil { + logging.Log().Warn("Was not able to marshal the interface.") + return false + } + for _, i := range interfaces { + jsonBytes, err := json.Marshal(i) + if err != nil { + logging.Log().Warn("Not able to marshal one of the intefaces.") + continue + } + if slices.Compare(jsonBytes, jsonBytesToCheck) == 0 { + return true + } + } + return false +} + +func parseAttributes(trustedIssuer tir.TrustedIssuer) (credentials []tir.Credential, err error) { + credentials = []tir.Credential{} + for _, attribute := range trustedIssuer.Attributes { + parsedCredential, err := parseAttribute(attribute) + if err != nil { + logging.Log().Warnf("Was not able to parse attribute %s. Err: %v", logging.PrettyPrintObject(attribute), err) + return credentials, err + } + credentials = append(credentials, parsedCredential) + } + return credentials, err +} + +func parseAttribute(attribute tir.IssuerAttribute) (credential tir.Credential, err error) { + decodedAttribute, err := base64.StdEncoding.DecodeString(attribute.Body) + if err != nil { + logging.Log().Warnf("The attribute body %s is not correctly base64 encoded. Err: %v", attribute.Body, err) + return credential, err + } + err = json.Unmarshal(decodedAttribute, &credential) + if err != nil { + logging.Log().Warnf("Was not able to unmarshal the credential %s. Err: %v", attribute.Body, err) + } + return } diff --git a/verifier/trustedissuer_test.go b/verifier/trustedissuer_test.go new file mode 100644 index 00000000..57d7a58a --- /dev/null +++ b/verifier/trustedissuer_test.go @@ -0,0 +1,130 @@ +package verifier + +import ( + "encoding/base64" + "encoding/json" + "errors" + "testing" + + "github.com/fiware/VCVerifier/logging" + tir "github.com/fiware/VCVerifier/tir" +) + +func TestVerifyVC_Issuers(t *testing.T) { + + type test struct { + testName string + credentialToVerifiy VerifiableCredential + verificationContext VerificationContext + tirExists bool + tirResponse tir.TrustedIssuer + tirError error + expectedResult bool + } + + tests := []test{ + {testName: "If no restriction is configured, the vc should be accepted.", + credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{})}), tirError: nil, expectedResult: true}, + {testName: "If the type is not included, the vc should be rejected.", + credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "AnotherType", map[string][]interface{}{})}), tirError: nil, expectedResult: false}, + {testName: "If all types are allowed, the vc should be allowed.", + credentialToVerifiy: getMultiTypeCredential([]string{"VerifiableCredential", "SecondType"}, "testClaim", "testValue"), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{}), getAttribute(tir.TimeRange{}, "SecondType", map[string][]interface{}{})}), tirError: nil, expectedResult: true}, + {testName: "If one of the types is not allowed, the vc should be rejected.", + credentialToVerifiy: getMultiTypeCredential([]string{"VerifiableCredential", "SecondType"}, "testClaim", "testValue"), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{})}), tirError: nil, expectedResult: false}, + {testName: "If no restricted claim is included, the vc should be accepted.", + credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"another": {"claim"}})}), tirError: nil, expectedResult: true}, + {testName: "If the (string)claim is allowed, the vc should be accepted.", + credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {"testValue"}})}), tirError: nil, expectedResult: true}, + {testName: "If the (string)claim is one of the allowed values, the vc should be accepted.", + credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {"testValue", "anotherAllowedValue"}})}), tirError: nil, expectedResult: true}, + {testName: "If the (string)claim is not allowed, the vc should be rejected.", + credentialToVerifiy: getVerifiableCredential("testClaim", "anotherValue"), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {"testValue"}})}), tirError: nil, expectedResult: false}, + {testName: "If the (number)claim is allowed, the vc should be accepted.", + credentialToVerifiy: getVerifiableCredential("testClaim", 1), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {1}})}), tirError: nil, expectedResult: true}, + {testName: "If the (number)claim is not allowed, the vc should be rejected.", + credentialToVerifiy: getVerifiableCredential("testClaim", 2), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {1}})}), tirError: nil, expectedResult: false}, + {testName: "If the (object)claim is allowed, the vc should be accepted.", + credentialToVerifiy: getVerifiableCredential("testClaim", map[string]interface{}{"some": "object"}), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"testClaim": {map[string]interface{}{"some": "object"}}})}), tirError: nil, expectedResult: true}, + {testName: "If the all claim allowed, the vc should be allowed.", + credentialToVerifiy: getMultiClaimCredential(map[string]interface{}{"claimA": map[string]interface{}{"some": "object"}, "claimB": "b"}), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"claimA": {map[string]interface{}{"some": "object"}}, "claimB": {"b"}})}), tirError: nil, expectedResult: true}, + {testName: "If not all claims are allowed, the vc should be rejected.", + credentialToVerifiy: getMultiClaimCredential(map[string]interface{}{"claimA": map[string]interface{}{"some": "object"}, "claimB": "b"}), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{"claimA": {map[string]interface{}{"some": "object"}}, "claimB": {"c"}})}), tirError: nil, expectedResult: false}, + {testName: "If the trusted-issuers-registry responds with an error, the vc should be rejected.", + credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), + tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{})}), tirError: errors.New("some-error"), expectedResult: false}, + {testName: "If an invalid verification context is provided, the credential should be rejected.", + credentialToVerifiy: getVerifiableCredential("test", "claim"), verificationContext: "No-context", tirExists: false, tirResponse: tir.TrustedIssuer{}, tirError: nil, expectedResult: false}, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + + logging.Log().Info("TestVerifyVC +++++++++++++++++ Running test: ", tc.testName) + + trustedIssuerVerficationService := TrustedIssuerVerificationService{mockTirClient{tc.tirExists, tc.tirResponse, tc.tirError}} + result, _ := trustedIssuerVerficationService.VerifyVC(tc.credentialToVerifiy, tc.verificationContext) + if result != tc.expectedResult { + t.Errorf("%s - Expected result %v but was %v.", tc.testName, tc.expectedResult, result) + return + } + }) + } +} + +func getAttribute(validFor tir.TimeRange, vcType string, claimsMap map[string][]interface{}) tir.IssuerAttribute { + claims := []tir.Claim{} + + for key, element := range claimsMap { + + claims = append(claims, tir.Claim{Name: key, AllowedValues: element}) + } + + credential := tir.Credential{ValidFor: validFor, CredentialsType: vcType, Claims: claims} + marshaledCredential, _ := json.Marshal(credential) + return tir.IssuerAttribute{Body: base64.StdEncoding.EncodeToString(marshaledCredential)} +} + +func getTrustedIssuer(attributes []tir.IssuerAttribute) tir.TrustedIssuer { + return tir.TrustedIssuer{Attributes: attributes} +} + +func getVerificationContext() VerificationContext { + return TrustRegistriesVerificationContext{trustedParticipantsRegistries: []string{"http://my-trust-registry.org"}} +} + +func getMultiTypeCredential(types []string, claimName string, value interface{}) VerifiableCredential { + vc := getVerifiableCredential(claimName, value) + vc.Types = types + return vc +} + +func getMultiClaimCredential(claims map[string]interface{}) VerifiableCredential { + return VerifiableCredential{ + MappableVerifiableCredential: MappableVerifiableCredential{ + Types: []string{"VerifiableCredential"}, + CredentialSubject: CredentialSubject{Claims: claims}, + }, + } +} + +func getVerifiableCredential(claimName string, value interface{}) VerifiableCredential { + return VerifiableCredential{ + MappableVerifiableCredential: MappableVerifiableCredential{ + Types: []string{"VerifiableCredential"}, + CredentialSubject: CredentialSubject{Claims: map[string]interface{}{claimName: value}}, + }, + } +} diff --git a/verifier/trustedparticipant.go b/verifier/trustedparticipant.go index 0fcdc519..c3c5e428 100644 --- a/verifier/trustedparticipant.go +++ b/verifier/trustedparticipant.go @@ -1,9 +1,14 @@ package verifier import ( + "errors" + + "github.com/fiware/VCVerifier/logging" tir "github.com/fiware/VCVerifier/tir" ) +var ErrorCannotConverContext = errors.New("cannot_convert_context") + /** * The trusted participant verification service will validate the entry of a participant within the trusted list. */ @@ -12,13 +17,17 @@ type TrustedParticipantVerificationService struct { } func (tpvs *TrustedParticipantVerificationService) VerifyVC(verifiableCredential VerifiableCredential, verificationContext VerificationContext) (result bool, err error) { + + defer func() { + if recErr := recover(); recErr != nil { + logging.Log().Warnf("Was not able to convert context. Err: %v", recErr) + err = ErrorCannotConverContext + } + }() trustContext := verificationContext.(TrustRegistriesVerificationContext) - exist, trustedIssuer, err := tpvs.tirClient.GetTrustedIssuer(trustContext.GetTrustedParticipantLists(), verifiableCredential.Issuer) - if err != nil { - return false, err - } - if !exist { - return false, err + if len(trustContext.trustedParticipantsRegistries) == 0 { + logging.Log().Debug("The verfication context does not specify a trusted issuers registry, therefor we consider every participant as trusted.") + return true, err } - trustedIssuer.Attributes + return tpvs.tirClient.IsTrustedParticipant(trustContext.GetTrustedParticipantLists(), verifiableCredential.Issuer), err } diff --git a/verifier/trustedparticipant_test.go b/verifier/trustedparticipant_test.go new file mode 100644 index 00000000..b32096f4 --- /dev/null +++ b/verifier/trustedparticipant_test.go @@ -0,0 +1,55 @@ +package verifier + +import ( + "testing" + + "github.com/fiware/VCVerifier/logging" + tir "github.com/fiware/VCVerifier/tir" +) + +type mockTirClient struct { + expectedExists bool + expectedIssuer tir.TrustedIssuer + expectedError error +} + +func (mtc mockTirClient) IsTrustedParticipant(tirEndpoints []string, did string) (trusted bool) { + return mtc.expectedExists +} + +func (mtc mockTirClient) GetTrustedIssuer(tirEndpoints []string, did string) (exists bool, trustedIssuer tir.TrustedIssuer, err error) { + return mtc.expectedExists, mtc.expectedIssuer, mtc.expectedError +} + +func TestVerifyVC_Participant(t *testing.T) { + + type test struct { + testName string + credentialToVerifiy VerifiableCredential + verificationContext VerificationContext + tirResponse bool + expectedResult bool + } + + tests := []test{ + {testName: "A credential issued by a registerd issuer should be successfully validated.", credentialToVerifiy: VerifiableCredential{raw: map[string]interface{}{"issuer": "did:web:trusted-issuer.org"}}, verificationContext: TrustRegistriesVerificationContext{trustedParticipantsRegistries: []string{"http://my-trust-registry.org"}}, tirResponse: true, expectedResult: true}, + {testName: "A credential issued by a not-registerd issuer should be rejected.", credentialToVerifiy: VerifiableCredential{raw: map[string]interface{}{"issuer": "did:web:trusted-issuer.org"}}, verificationContext: TrustRegistriesVerificationContext{trustedParticipantsRegistries: []string{"http://my-trust-registry.org"}}, tirResponse: false, expectedResult: false}, + {testName: "If no registry is configured, the credential should be accepted.", credentialToVerifiy: VerifiableCredential{raw: map[string]interface{}{"issuer": "did:web:trusted-issuer.org"}}, verificationContext: TrustRegistriesVerificationContext{trustedParticipantsRegistries: []string{}}, tirResponse: true, expectedResult: true}, + {testName: "If no registry is configured, the credential should be accepted.", credentialToVerifiy: VerifiableCredential{raw: map[string]interface{}{"issuer": "did:web:trusted-issuer.org"}}, verificationContext: TrustRegistriesVerificationContext{trustedParticipantsRegistries: []string{}}, tirResponse: false, expectedResult: true}, + {testName: "If an invalid context is received, the credential should be rejected.", credentialToVerifiy: VerifiableCredential{raw: map[string]interface{}{"issuer": "did:web:trusted-issuer.org"}}, verificationContext: "No-Context", tirResponse: false, expectedResult: false}, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + + logging.Log().Info("TestVerifyVC +++++++++++++++++ Running test: ", tc.testName) + + trustedParticipantVerificationService := TrustedParticipantVerificationService{mockTirClient{tc.tirResponse, tir.TrustedIssuer{}, nil}} + result, _ := trustedParticipantVerificationService.VerifyVC(tc.credentialToVerifiy, tc.verificationContext) + if result != tc.expectedResult { + t.Errorf("%s - Expected result %v but was %v.", tc.testName, tc.expectedResult, result) + return + } + }) + } +} diff --git a/verifier/verifiable_credential.go b/verifier/verifiable_credential.go index df27c2ad..36823f03 100644 --- a/verifier/verifiable_credential.go +++ b/verifier/verifiable_credential.go @@ -25,8 +25,9 @@ type MappableVerifiableCredential struct { // 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"` + Claims map[string]interface{} `mapstructure:",remain"` } func optionalFields() []string { diff --git a/verifier/verifiable_credential_test.go b/verifier/verifiable_credential_test.go index 0ca2e944..c1d47314 100644 --- a/verifier/verifiable_credential_test.go +++ b/verifier/verifiable_credential_test.go @@ -151,6 +151,7 @@ func TestMapVerifiableCredential(t *testing.T) { CredentialSubject: CredentialSubject{ Id: "someId", SubjectType: "gx:compliance", + Claims: map[string]interface{}{"target": "did:ebsi:packetdelivery"}, }, }, exampleCredential, @@ -188,6 +189,7 @@ func TestMapVerifiableCredential(t *testing.T) { CredentialSubject: CredentialSubject{ Id: "someId", SubjectType: "gx:compliance", + Claims: map[string]interface{}{"target": "did:ebsi:packetdelivery"}, }, }, exampleCredentialArraySubject, diff --git a/verifier/verifier.go b/verifier/verifier.go index dcbe0c60..ba0e9613 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -13,6 +13,7 @@ import ( "time" configModel "github.com/fiware/VCVerifier/config" + "github.com/fiware/VCVerifier/tir" logging "github.com/fiware/VCVerifier/logging" @@ -197,9 +198,16 @@ func InitVerifier(verifierConfig *configModel.Verifier, repoConfig *configModel. logging.Log().Errorf("Was not able to initiate the credentials config. Err: %v", err) } - trustedParticipantsVerificationService := TrustedParticipantVerificationService{} + tirClient, err := tir.NewTirHttpClient() + if err != nil { + logging.Log().Errorf("Was not able to instantiate the trusted-issuers-registry client. Err: %v", err) + return err + } + trustedParticipantVerificationService := TrustedParticipantVerificationService{tirClient: tirClient} + trustedIssuerVerificationService := TrustedIssuerVerificationService{tirClient: tirClient} key, err := initPrivateKey() + if err != nil { logging.Log().Errorf("Was not able to initiate a signing key. Err: %v", err) return err @@ -219,7 +227,8 @@ func InitVerifier(verifierConfig *configModel.Verifier, repoConfig *configModel. []VerificationService{ &externalSsiKitVerifier, &externalGaiaXVerifier, - &trustedParticipantsVerificationService, + &trustedParticipantVerificationService, + &trustedIssuerVerificationService, }, } diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 1b05377e..2df5dc34 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -125,13 +125,14 @@ func TestInitSiopFlow(t *testing.T) { tests := getInitSiopTests() for _, tc := range tests { - - logging.Log().Info("TestInitSiopFlow +++++++++++++++++ Running test: ", tc.testName) - sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} - nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} - 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) + t.Run(tc.testName, func(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 := 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) + }) } } @@ -142,13 +143,14 @@ func TestStartSiopFlow(t *testing.T) { tests := getInitSiopTests() for _, tc := range tests { - logging.Log().Info("TestStartSiopFlow +++++++++++++++++ Running test: ", tc.testName) - - sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} - nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} - 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) + t.Run(tc.testName, func(t *testing.T) { + logging.Log().Info("TestStartSiopFlow +++++++++++++++++ Running test: ", tc.testName) + sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} + nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} + 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) + }) } } @@ -208,13 +210,14 @@ func TestStartSameDeviceFlow(t *testing.T) { } for _, tc := range tests { - - logging.Log().Info("TestSameDeviceFlow +++++++++++++++++ Running test: ", tc.testName) - sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} - nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} - 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) + t.Run(tc.testName, func(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 := 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) + }) } } @@ -321,40 +324,41 @@ func TestAuthenticationResponse(t *testing.T) { } for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + logging.Log().Info("TestAuthenticationResponse +++++++++++++++++ Running test: ", tc.testName) + sessionCache := mockSessionCache{sessions: map[string]loginSession{}} - logging.Log().Info("TestAuthenticationResponse +++++++++++++++++ Running test: ", tc.testName) - sessionCache := mockSessionCache{sessions: map[string]loginSession{}} + // initialize siop session + if tc.testSession != (loginSession{}) { + sessionCache.sessions[tc.testState] = tc.testSession + } - // initialize siop session - if tc.testSession != (loginSession{}) { - sessionCache.sessions[tc.testState] = tc.testSession - } - - tokenCache := mockTokenCache{tokens: map[string]tokenStore{}, errorToThrow: tc.tokenCacheError} + tokenCache := mockTokenCache{tokens: map[string]tokenStore{}, errorToThrow: tc.tokenCacheError} - httpClient = mockHttpClient{tc.callbackError, nil} - ecdsKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - testKey, _ := jwk.New(ecdsKey) - jwk.AssignKeyID(testKey) - nonceGenerator := mockNonceGenerator{staticValues: []string{"authCode"}} - verifier := CredentialVerifier{did: "did:key:verifier", signingKey: testKey, tokenCache: &tokenCache, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, verificationServices: []VerificationService{&mockExternalSsiKit{tc.verificationResult, tc.verificationError}}, clock: mockClock{}} + httpClient = mockHttpClient{tc.callbackError, nil} + ecdsKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + testKey, _ := jwk.New(ecdsKey) + jwk.AssignKeyID(testKey) + nonceGenerator := mockNonceGenerator{staticValues: []string{"authCode"}} + verifier := CredentialVerifier{did: "did:key:verifier", signingKey: testKey, tokenCache: &tokenCache, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, verificationServices: []VerificationService{&mockExternalSsiKit{tc.verificationResult, tc.verificationError}}, clock: mockClock{}} - sameDeviceResponse, err := verifier.AuthenticationResponse(tc.requestedState, tc.testVC, tc.testHolder) - if err != tc.expectedError { - t.Errorf("%s - Expected error %v but was %v.", tc.testName, tc.expectedError, err) - } - if tc.expectedError != nil { - continue - } + sameDeviceResponse, err := verifier.AuthenticationResponse(tc.requestedState, tc.testVC, tc.testHolder) + if err != tc.expectedError { + t.Errorf("%s - Expected error %v but was %v.", tc.testName, tc.expectedError, err) + } + if tc.expectedError != nil { + return + } - if tc.sameDevice { - verifySameDevice(t, sameDeviceResponse, tokenCache, tc) - continue - } + if tc.sameDevice { + verifySameDevice(t, sameDeviceResponse, tokenCache, tc) + return + } - if *tc.expectedCallback != *lastRequest { - t.Errorf("%s - Expected callback %s but was %s.", tc.testName, tc.expectedCallback, lastRequest) - } + if *tc.expectedCallback != *lastRequest { + t.Errorf("%s - Expected callback %s but was %s.", tc.testName, tc.expectedCallback, lastRequest) + } + }) } } @@ -414,25 +418,26 @@ func TestInitVerifier(t *testing.T) { } for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + verifier = nil + logging.Log().Info("TestInitVerifier +++++++++++++++++ Running test: ", tc.testName) - verifier = nil - logging.Log().Info("TestInitVerifier +++++++++++++++++ Running test: ", tc.testName) - - err := InitVerifier(&tc.testConfig, &configModel.ConfigRepo{}, &mockSsiKit{}) - if tc.expectedError != err { - t.Errorf("%s - Expected error %v but was %v.", tc.testName, tc.expectedError, err) - } - if tc.expectedError != nil && GetVerifier() != nil { - t.Errorf("%s - When an error happens, no verifier should be created.", tc.testName) - continue - } - if tc.expectedError != nil { - continue - } + err := InitVerifier(&tc.testConfig, &configModel.ConfigRepo{}, &mockSsiKit{}) + if tc.expectedError != err { + t.Errorf("%s - Expected error %v but was %v.", tc.testName, tc.expectedError, err) + } + if tc.expectedError != nil && GetVerifier() != nil { + t.Errorf("%s - When an error happens, no verifier should be created.", tc.testName) + return + } + if tc.expectedError != nil { + return + } - if GetVerifier() == nil { - t.Errorf("%s - Verifier should have been initiated, but is not available.", tc.testName) - } + if GetVerifier() == nil { + t.Errorf("%s - Verifier should have been initiated, but is not available.", tc.testName) + } + }) } } @@ -506,36 +511,37 @@ func TestGetToken(t *testing.T) { } for _, tc := range tests { - - logging.Log().Info("TestGetToken +++++++++++++++++ Running test: ", tc.testName) - - tokenCache := mockTokenCache{tokens: tc.tokenSession} - 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 { - t.Errorf("%s - Expected error %v but was %v.", tc.testName, tc.expectedError, err) - continue - } - if tc.expectedError != nil { - // we successfully verified that it failed. - continue - } - - returnedToken, err := jwt.Parse([]byte(jwtString), jwt.WithVerify(jwa.ES256, publicKey)) - - if err != nil { - t.Errorf("%s - No valid token signature. Err: %v", tc.testName, err) - continue - } - if logging.PrettyPrintObject(returnedToken) != logging.PrettyPrintObject(tc.expectedJWT) { - t.Errorf("%s - Expected jwt %s but was %s.", tc.testName, logging.PrettyPrintObject(tc.expectedJWT), logging.PrettyPrintObject(returnedToken)) - continue - } - if expiration != tc.expectedExpiration { - t.Errorf("%s - Expected expiration %v but was %v.", tc.testName, tc.expectedExpiration, expiration) - continue - } + t.Run(tc.testName, func(t *testing.T) { + logging.Log().Info("TestGetToken +++++++++++++++++ Running test: ", tc.testName) + + tokenCache := mockTokenCache{tokens: tc.tokenSession} + 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 { + t.Errorf("%s - Expected error %v but was %v.", tc.testName, tc.expectedError, err) + return + } + if tc.expectedError != nil { + // we successfully verified that it failed. + return + } + + returnedToken, err := jwt.Parse([]byte(jwtString), jwt.WithVerify(jwa.ES256, publicKey)) + + if err != nil { + t.Errorf("%s - No valid token signature. Err: %v", tc.testName, err) + return + } + if logging.PrettyPrintObject(returnedToken) != logging.PrettyPrintObject(tc.expectedJWT) { + t.Errorf("%s - Expected jwt %s but was %s.", tc.testName, logging.PrettyPrintObject(tc.expectedJWT), logging.PrettyPrintObject(returnedToken)) + return + } + if expiration != tc.expectedExpiration { + t.Errorf("%s - Expected expiration %v but was %v.", tc.testName, tc.expectedExpiration, expiration) + return + } + }) } } From a2e8de84b073a617f5b6b005e4197fc28b3ee68e Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 26 May 2023 14:46:53 +0200 Subject: [PATCH 3/8] add error checks --- verifier/trustedissuer_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/verifier/trustedissuer_test.go b/verifier/trustedissuer_test.go index 57d7a58a..3ef78695 100644 --- a/verifier/trustedissuer_test.go +++ b/verifier/trustedissuer_test.go @@ -23,6 +23,12 @@ func TestVerifyVC_Issuers(t *testing.T) { } tests := []test{ + {testName: "If no trusted issuer is configured in the list, the vc should be rejected.", + credentialToVerifiy: getVerifiableCredential("test", "claim"), verificationContext: getVerificationContext(), + tirExists: false, tirResponse: tir.TrustedIssuer{}, tirError: nil, expectedResult: false}, + {testName: "If the trusted issuer is invalid, the vc should be rejected.", + credentialToVerifiy: getVerifiableCredential("test", "claim"), verificationContext: getVerificationContext(), + tirExists: false, tirResponse: tir.TrustedIssuer{Attributes: []tir.IssuerAttribute{{Body: "invalidBody"}}}, tirError: nil, expectedResult: false}, {testName: "If no restriction is configured, the vc should be accepted.", credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{})}), tirError: nil, expectedResult: true}, From f02a1769a99efb3329796c2becba5795e298647b Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 26 May 2023 14:51:48 +0200 Subject: [PATCH 4/8] fix it --- verifier/trustedissuer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verifier/trustedissuer_test.go b/verifier/trustedissuer_test.go index 3ef78695..996a15aa 100644 --- a/verifier/trustedissuer_test.go +++ b/verifier/trustedissuer_test.go @@ -28,7 +28,7 @@ func TestVerifyVC_Issuers(t *testing.T) { tirExists: false, tirResponse: tir.TrustedIssuer{}, tirError: nil, expectedResult: false}, {testName: "If the trusted issuer is invalid, the vc should be rejected.", credentialToVerifiy: getVerifiableCredential("test", "claim"), verificationContext: getVerificationContext(), - tirExists: false, tirResponse: tir.TrustedIssuer{Attributes: []tir.IssuerAttribute{{Body: "invalidBody"}}}, tirError: nil, expectedResult: false}, + tirExists: true, tirResponse: tir.TrustedIssuer{Attributes: []tir.IssuerAttribute{{Body: "invalidBody"}}}, tirError: nil, expectedResult: false}, {testName: "If no restriction is configured, the vc should be accepted.", credentialToVerifiy: getVerifiableCredential("testClaim", "testValue"), verificationContext: getVerificationContext(), tirExists: true, tirResponse: getTrustedIssuer([]tir.IssuerAttribute{getAttribute(tir.TimeRange{}, "VerifiableCredential", map[string][]interface{}{})}), tirError: nil, expectedResult: true}, From 21f03b121e931bf6c9ad394d9bcd521ecb7b8d7c Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 26 May 2023 16:07:30 +0200 Subject: [PATCH 5/8] verify with the configured context --- README.md | 35 +++++++++++- api/api.yaml | 11 ++++ config/config.go | 2 - config/provider_test.go | 2 - openapi/api_api.go | 14 ++++- openapi/api_api_test.go | 6 +- openapi/api_frontend.go | 8 ++- verifier/verifier.go | 83 ++++++++++++++++++--------- verifier/verifier_test.go | 114 ++++++++++++++++++++++++-------------- 9 files changed, 196 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 46a3ae9f..811c5d97 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,11 @@ The following actions occur in the interaction: 2. The frontend-application forwards the user to the login-page of VCVerifier 3. The VCVerifier presents a QR-code, containing the ```openid:```-connection string with all necessary information to start the authentication process. The QR-code is scanned by the user's wallet. 4. The user approves the wallet's interaction with the VCVerifier and the VerifiableCredential is presented via the OIDC4VP-flow. -5. VCVerifier requests verification of the credential with a defined set of policies at WaltID-SSIKit. +5. VCVerifier verifies the credential: + 1. at WaltID-SSIKit with the configured set of policies + 2. (Optional) if a Gaia-X compliant chain is provided + 3. that the credential is registered in the configured trusted-participants-registries + 4. that the issuer is allowed to issuer the credential with the given claims by one of the configured trusted-issuers-list(s) 6. A JWT is created, the frontend-application is informed via callback and the token is retrieved via the token-endpoint. 7. Frontend start to interact with the backend-service, using the jwt. 8. Authorization-Layer requests the JWKS from the VCVerifier(this can happen asynchronously, not in the sequential flow of the diagram). @@ -111,6 +115,35 @@ ssiKit: # url of the ssikit auditor-api(see https://docs.walt.id/v/ssikit/getting-started/rest-apis/auditor-api) auditorURL: +# configuration of the service to retrieve configuration for +configRepo: + # endpoint of the configuration service, to retrieve the scope to be requested and the trust endpoints for the credentials. + configEndpoint: http://config-service:8080 + # static configuration for services + services: + # name of the service to be configured + testService: + # scope to be requested from the wallet + scope: + - VerifiableCredential + - CustomerCredential + # trusted participants endpoint configuration + trustedParticipants: + # the credentials type to configure the endpoint(s) for + VerifiableCredential: + - https://tir-pdc.gaia-x.fiware.dev + # the credentials type to configure the endpoint(s) for + CustomerCredential: + - https://tir-pdc.gaia-x.fiware.dev + # trusted issuers endpoint configuration + trustedIssuers: + # the credentials type to configure the endpoint(s) for + VerifiableCredential: + - https://tir-pdc.gaia-x.fiware.dev + # the credentials type to configure the endpoint(s) for + CustomerCredential: + - https://tir-pdc.gaia-x.fiware.dev + ``` #### Templating diff --git a/api/api.yaml b/api/api.yaml index 5ee6fee7..238d3297 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -17,6 +17,7 @@ paths: parameters: - $ref: '#/components/parameters/QueryState' - $ref: '#/components/parameters/ClientCallback' + - $ref: '#/components/parameters/ClientId' operationId: VerifierPageDisplayQRSIOP summary: Presents a qr as starting point for the auth process description: Returns a rendered html with a QR encoding the login-starting point for the siop flow - e.g. 'openid://?scope=somethign&response_type=rt&response_mode=rm&client_id=ci&redirect_uri=uri&state=state&nonce=nonce' @@ -40,6 +41,7 @@ paths: - api parameters: - $ref: '#/components/parameters/QueryState' + - $ref: '#/components/parameters/ClientId' operationId: StartSIOPSameDevice summary: Starts the siop flow for credentials hold by the same device description: When the credential is already present in the requesting browser, the same-device flow can be used. It creates the login information and then redirects to the /authenticationresponse path. @@ -71,6 +73,7 @@ paths: - api parameters: - $ref: '#/components/parameters/QueryState' + - $ref: '#/components/parameters/ClientId' operationId: VerifierAPIAuthenticationResponse summary: Stores the credential for the given session requestBody: @@ -208,6 +211,14 @@ components: required: true schema: type: string + ClientId: + name: client_id + description: The id of the client/service that intents to start the authentication flow. Will be used to retrieve the scope and trust services to be used for verification. + in: query + required: false + schema: + type: string + example: packet-delivery-portal schemas: CredentialsType: type: array diff --git a/config/config.go b/config/config.go index 4117c5bd..1a226d00 100644 --- a/config/config.go +++ b/config/config.go @@ -47,8 +47,6 @@ type Verifier struct { TirAddress string `mapstructure:"tirAddress"` // expiry of auth sessions 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"` } diff --git a/config/provider_test.go b/config/provider_test.go index 9b5cc162..422074aa 100644 --- a/config/provider_test.go +++ b/config/provider_test.go @@ -30,7 +30,6 @@ func Test_ReadConfig(t *testing.T) { Did: "did:key:somekey", TirAddress: "https://test.dev/trusted_issuer/v3/issuers/", SessionExpiry: 30, - RequestScope: "", PolicyConfig: Policies{ DefaultPolicies: PolicyMap{ "SignaturePolicy": {}, @@ -80,7 +79,6 @@ func Test_ReadConfig(t *testing.T) { Verifier: Verifier{Did: "", TirAddress: "", SessionExpiry: 30, - RequestScope: "", }, SSIKit: SSIKit{ AuditorURL: "", }, diff --git a/openapi/api_api.go b/openapi/api_api.go index 754bb404..a2f1d748 100644 --- a/openapi/api_api.go +++ b/openapi/api_api.go @@ -90,7 +90,12 @@ func StartSIOPSameDevice(c *gin.Context) { protocol = "http" } - redirect, err := getApiVerifier().StartSameDeviceFlow(c.Request.Host, protocol, state, redirectPath) + clientId, clientIdExists := c.GetQuery("client_id") + if !clientIdExists { + logging.Log().Infof("Start a login flow for a not specified client.") + } + + redirect, err := getApiVerifier().StartSameDeviceFlow(c.Request.Host, protocol, state, redirectPath, clientId) if err != nil { logging.Log().Warnf("Error starting the same-device flow. Err: %v", err) c.AbortWithStatusJSON(500, ErrorMessage{err.Error(), "Was not able to start the same device flow."}) @@ -202,7 +207,12 @@ func VerifierAPIStartSIOP(c *gin.Context) { if c.Request.TLS == nil { protocol = "http" } - connectionString, err := getApiVerifier().StartSiopFlow(c.Request.Host, protocol, callback, state) + clientId, clientIdExists := c.GetQuery("client_id") + if !clientIdExists { + logging.Log().Infof("Start a login flow for a not specified client.") + } + + connectionString, err := getApiVerifier().StartSiopFlow(c.Request.Host, protocol, callback, state, clientId) if err != nil { c.AbortWithStatusJSON(500, ErrorMessage{err.Error(), "Was not able to generate the connection string."}) return diff --git a/openapi/api_api_test.go b/openapi/api_api_test.go index ceab20da..6fe8457f 100644 --- a/openapi/api_api_test.go +++ b/openapi/api_api_test.go @@ -28,13 +28,13 @@ type mockVerifier struct { mockError error } -func (mV *mockVerifier) ReturnLoginQR(host string, protocol string, callback string, sessionId string) (qr string, err error) { +func (mV *mockVerifier) ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string) (qr string, err error) { return mV.mockQR, mV.mockError } -func (mV *mockVerifier) StartSiopFlow(host string, protocol string, callback string, sessionId string) (connectionString string, err error) { +func (mV *mockVerifier) StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string) (connectionString string, err error) { return mV.mockConnectionString, mV.mockError } -func (mV *mockVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string) (authenticationRequest string, err error) { +func (mV *mockVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string) (authenticationRequest string, err error) { return mV.mockAuthRequest, mV.mockError } func (mV *mockVerifier) GetToken(grantType string, authorizationCode string, redirectUri string) (jwtString string, expiration int64, err error) { diff --git a/openapi/api_frontend.go b/openapi/api_frontend.go index 21d242c0..7e3898fc 100644 --- a/openapi/api_frontend.go +++ b/openapi/api_frontend.go @@ -12,6 +12,7 @@ package openapi import ( "net/http" + "github.com/fiware/VCVerifier/logging" "github.com/fiware/VCVerifier/verifier" "github.com/gin-gonic/gin" @@ -43,7 +44,12 @@ func VerifierPageDisplayQRSIOP(c *gin.Context) { return } - qr, err := getFrontendVerifier().ReturnLoginQR(c.Request.Host, "https", callback, state) + clientId, clientIdExists := c.GetQuery("client_id") + if !clientIdExists { + logging.Log().Infof("Start a login flow for a not specified client.") + } + + qr, err := getFrontendVerifier().ReturnLoginQR(c.Request.Host, "https", callback, state, clientId) if err != nil { c.AbortWithStatusJSON(500, ErrorMessage{"qr_generation_error", err.Error()}) return diff --git a/verifier/verifier.go b/verifier/verifier.go index ba0e9613..16755ccc 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -10,6 +10,7 @@ import ( "io" "net/http" "net/url" + "strings" "time" configModel "github.com/fiware/VCVerifier/config" @@ -30,20 +31,21 @@ import ( ) var ErrorNoDID = errors.New("no_did_configured") -var ErrorNoTIR = errors.New("nod_tir_configured") +var ErrorNoTIR = errors.New("no_tir_configured") var ErrorInvalidVC = errors.New("invalid_vc") var ErrorNoSuchSession = errors.New("no_such_session") var ErrorWrongGrantType = errors.New("wrong_grant_type") var ErrorNoSuchCode = errors.New("no_such_code") var ErrorRedirectUriMismatch = errors.New("redirect_uri_does_not_match") +var ErrorVerficationContextSetup = errors.New("no_valid_verification_context") // Actual implementation of the verfifier functionality // verifier interface type Verifier interface { - ReturnLoginQR(host string, protocol string, callback string, sessionId string) (qr string, err error) - StartSiopFlow(host string, protocol string, callback string, sessionId string) (connectionString string, err error) - StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string) (authenticationRequest string, err error) + ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string) (qr string, err error) + StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string) (connectionString string, err error) + StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string) (authenticationRequest string, err error) GetToken(grantType string, authorizationCode string, redirectUri string) (jwtString string, expiration int64, err error) GetJWKS() jwk.Set AuthenticationResponse(state string, verifiableCredentials []map[string]interface{}, holder string) (sameDevice SameDeviceResponse, err error) @@ -60,8 +62,6 @@ type CredentialVerifier struct { did string // trusted-issuers-registry to be used for verification tirAddress string - // optional scope of credentials to be requested - scope string // key to sign the jwt's with signingKey jwk.Key // cache to be used for in-progress authentication sessions @@ -145,6 +145,8 @@ type loginSession struct { callback string // sessionId to be included in the notification sessionId string + // clientId provided for the session + clientId string } // struct to represent a token, accessible through the token endpoint @@ -216,7 +218,6 @@ func InitVerifier(verifierConfig *configModel.Verifier, repoConfig *configModel. verifier = &CredentialVerifier{ verifierConfig.Did, verifierConfig.TirAddress, - verifierConfig.RequestScope, key, sessionCache, tokenCache, @@ -239,10 +240,10 @@ func InitVerifier(verifierConfig *configModel.Verifier, repoConfig *configModel. /** * Initializes the cross-device login flow and returns all neccessary information as a qr-code **/ -func (v *CredentialVerifier) 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, clientId string) (qr string, err error) { logging.Log().Debugf("Generate a login qr for %s.", callback) - authenticationRequest, err := v.initSiopFlow(host, protocol, callback, sessionId) + authenticationRequest, err := v.initSiopFlow(host, protocol, callback, sessionId, clientId) if err != nil { return qr, err @@ -258,20 +259,20 @@ func (v *CredentialVerifier) ReturnLoginQR(host string, protocol string, callbac /** * Starts a siop-flow and returns the required connection information **/ -func (v *CredentialVerifier) 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, clientId string) (connectionString string, err error) { logging.Log().Debugf("Start a plain siop-flow fro %s.", callback) - return v.initSiopFlow(host, protocol, callback, sessionId) + return v.initSiopFlow(host, protocol, callback, sessionId, clientId) } /** * Starts a same-device siop-flow and returns the required redirection information **/ -func (v *CredentialVerifier) 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, clientId string) (authenticationRequest string, err error) { logging.Log().Debugf("Initiate samedevice flow for %s.", host) state := v.nonceGenerator.GenerateNonce() - loginSession := loginSession{true, fmt.Sprintf("%s://%s%s", protocol, host, redirectPath), sessionId} + loginSession := loginSession{true, fmt.Sprintf("%s://%s%s", protocol, host, redirectPath), sessionId, clientId} err = v.sessionCache.Add(state, loginSession, cache.DefaultExpiration) if err != nil { logging.Log().Warnf("Was not able to store the login session %s in cache. Err: %v", logging.PrettyPrintObject(loginSession), err) @@ -281,7 +282,7 @@ func (v *CredentialVerifier) StartSameDeviceFlow(host string, protocol string, s redirectUri := fmt.Sprintf("%s://%s/api/v1/authentication_response", protocol, host) walletUri := protocol + "://" + host + redirectPath - return v.createAuthenticationRequest(walletUri, redirectUri, state), err + return v.createAuthenticationRequest(walletUri, redirectUri, state, clientId), err } /** @@ -352,9 +353,14 @@ func (v *CredentialVerifier) AuthenticationResponse(state string, verifiableCred } for _, mappedCredential := range mappedCredentials { + verificationContext, err := v.getTrustRegistriesVerificationContext(loginSession.clientId, mappedCredential.Types) + if err != nil { + logging.Log().Warnf("Was not able to create a valid verification context. Credential will be rejected. Err: %v", err) + return sameDevice, ErrorVerficationContextSetup + } //FIXME make it an error if no policy was checked at all( possible misconfiguration) for _, verificationService := range v.verificationServices { - result, err := verificationService.VerifyVC(mappedCredential, TrustRegistriesVerificationContext{}) + result, err := verificationService.VerifyVC(mappedCredential, verificationContext) if err != nil { logging.Log().Warnf("Failed to verify credential %s. Err: %v", logging.PrettyPrintObject(mappedCredential), err) return sameDevice, err @@ -402,6 +408,28 @@ func (v *CredentialVerifier) AuthenticationResponse(state string, verifiableCred } } +func (v *CredentialVerifier) getTrustRegistriesVerificationContext(clientId string, credentialTypes []string) (verificationContext TrustRegistriesVerificationContext, err error) { + trustedIssuersLists := []string{} + trustedParticipantsRegistries := []string{} + + for _, credentialType := range credentialTypes { + issuersLists, err := v.credentialsConfig.GetTrustedIssuersLists(clientId, credentialType) + if err != nil { + logging.Log().Warnf("Was not able to get valid trusted-issuers-lists for client %s and type %s. Err: %v", clientId, credentialType, err) + return verificationContext, err + } + participantsLists, err := v.credentialsConfig.GetTrustedParticipantLists(clientId, credentialType) + if err != nil { + logging.Log().Warnf("Was not able to get valid trusted-pariticpants-registries for client %s and type %s. Err: %v", clientId, credentialType, err) + return verificationContext, err + } + trustedIssuersLists = append(trustedIssuersLists, issuersLists...) + trustedParticipantsRegistries = append(trustedParticipantsRegistries, participantsLists...) + } + + return TrustRegistriesVerificationContext{trustedIssuersLists: trustedIssuersLists, trustedParticipantsRegistries: trustedParticipantsRegistries}, err +} + // 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 { @@ -437,10 +465,10 @@ func verifyChain(vcs []VerifiableCredential) (bool, error) { } // initializes the cross-device siop flow -func (v *CredentialVerifier) 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, clientId string) (authenticationRequest string, err error) { state := v.nonceGenerator.GenerateNonce() - loginSession := loginSession{false, callback, sessionId} + loginSession := loginSession{false, callback, sessionId, clientId} err = v.sessionCache.Add(state, loginSession, cache.DefaultExpiration) if err != nil { @@ -448,7 +476,7 @@ func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback return authenticationRequest, err } redirectUri := fmt.Sprintf("%s://%s/api/v1/authentication_response", protocol, host) - authenticationRequest = v.createAuthenticationRequest("openid://", redirectUri, state) + authenticationRequest = v.createAuthenticationRequest("openid://", redirectUri, state, clientId) logging.Log().Debugf("Authentication request is %s.", authenticationRequest) return authenticationRequest, err @@ -458,9 +486,7 @@ func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback 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) - } + if len(verifiableCredentials) > 1 { jwtBuilder.Claim("verifiablePresentation", verifiableCredentials) } else { @@ -477,7 +503,7 @@ func (v *CredentialVerifier) generateJWT(verifiableCredentials []map[string]inte } // creates an authenticationRequest string from the given parameters -func (v *CredentialVerifier) createAuthenticationRequest(base string, redirect_uri string, state string) string { +func (v *CredentialVerifier) createAuthenticationRequest(base string, redirect_uri string, state string, clientId string) string { // We use a template to generate the final string template := "{{base}}?response_type=vp_token" + @@ -487,14 +513,21 @@ func (v *CredentialVerifier) createAuthenticationRequest(base string, redirect_u "&state={{state}}" + "&nonce={{nonce}}" - if v.scope != "" { - template = template + "&scope={{scope}}" + var scope string + if clientId != "" { + typesToBeRequested, err := v.credentialsConfig.GetScope(clientId) + if err != nil { + logging.Log().Warnf("Was not able to get the scope to be requested for client %s. Err: %v", clientId, err) + } else { + template = template + "&scope={{scope}}" + scope = strings.Join(typesToBeRequested, ",") + } } t := fasttemplate.New(template, "{{", "}}") authRequest := t.ExecuteString(map[string]interface{}{ "base": base, - "scope": v.scope, + "scope": scope, "client_id": v.did, "redirect_uri": redirect_uri, "state": state, diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 2df5dc34..d401c1b2 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -37,13 +37,15 @@ func TestVerifyConfig(t *testing.T) { } for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + logging.Log().Info("TestVerifyConfig +++++++++++++++++ Running test: ", tc.testName) - logging.Log().Info("TestVerifyConfig +++++++++++++++++ Running test: ", tc.testName) + verificationResult := verifyConfig(&tc.configToTest) + if verificationResult != tc.expectedError { + t.Errorf("%s - Expected %v but was %v.", tc.testName, tc.expectedError, verificationResult) + } + }) - verificationResult := verifyConfig(&tc.configToTest) - if verificationResult != tc.expectedError { - t.Errorf("%s - Expected %v but was %v.", tc.testName, tc.expectedError, verificationResult) - } } } @@ -71,6 +73,23 @@ type mockTokenCache struct { tokens map[string]tokenStore errorToThrow error } +type mockCredentialConfig struct { + mockScopes map[string][]string + mockError error +} + +func (mcc mockCredentialConfig) GetScope(serviceIdentifier string) (credentialTypes []string, err error) { + if mcc.mockError != nil { + return credentialTypes, mcc.mockError + } + return mcc.mockScopes[serviceIdentifier], err +} +func (mcc mockCredentialConfig) GetTrustedParticipantLists(serviceIdentifier string, credentialType string) (trustedIssuersRegistryUrl []string, err error) { + return trustedIssuersRegistryUrl, err +} +func (mcc mockCredentialConfig) GetTrustedIssuersLists(serviceIdentifier string, credentialType string) (trustedIssuersRegistryUrl []string, err error) { + return trustedIssuersRegistryUrl, err +} func (msc *mockSessionCache) Add(k string, x interface{}, d time.Duration) error { if msc.errorToThrow != nil { @@ -112,7 +131,9 @@ type siopInitTest struct { testProtocol string testAddress string testSessionId string - scopeConfig string + testClientId string + credentialScopes map[string][]string + mockConfigError error expectedCallback string expectedConnection string sessionCacheError error @@ -129,8 +150,9 @@ 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 := CredentialVerifier{did: "did:key:verifier", scope: tc.scopeConfig, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator} - authReq, err := verifier.initSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId) + credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} + verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, credentialsConfig: credentialsConfig} + authReq, err := verifier.initSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId) verifyInitTest(t, tc, authReq, err, sessionCache, false) }) } @@ -147,8 +169,9 @@ func TestStartSiopFlow(t *testing.T) { logging.Log().Info("TestStartSiopFlow +++++++++++++++++ Running test: ", tc.testName) sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} - 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) + credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} + verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, credentialsConfig: credentialsConfig} + authReq, err := verifier.StartSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId) verifyInitTest(t, tc, authReq, err, sessionCache, false) }) } @@ -169,7 +192,7 @@ func verifyInitTest(t *testing.T, tc siopInitTest, authRequest string, err error if !found { t.Errorf("%s - A login session should have been stored.", tc.testName) } - expectedSession := loginSession{sameDevice, tc.expectedCallback, tc.testSessionId} + expectedSession := loginSession{sameDevice, tc.expectedCallback, tc.testSessionId, tc.testClientId} if cachedSession != expectedSession { t.Errorf("%s - The login session was expected to be %v but was %v.", tc.testName, expectedSession, cachedSession) } @@ -180,14 +203,17 @@ func getInitSiopTests() []siopInitTest { cacheFailError := errors.New("cache_fail") return []siopInitTest{ - {"If all parameters are set, a proper connection string should be returned.", "verifier.org", "https", "https://client.org/callback", "my-super-random-id", "", "https://client.org/callback", - "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", nil, nil, + {testName: "If all parameters are set, a proper connection string should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", credentialScopes: map[string][]string{}, mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, + }, + {testName: "The scope should be included if configured.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "myService", credentialScopes: map[string][]string{"myService": {"org.fiware.MySpecialCredential"}}, mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce&scope=org.fiware.MySpecialCredential", sessionCacheError: nil, expectedError: nil, }, - {"The scope should be included if configured.", "verifier.org", "https", "https://client.org/callback", "my-super-random-id", "org.fiware.MySpecialCredential", "https://client.org/callback", - "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce&scope=org.fiware.MySpecialCredential", nil, nil, + {testName: "If the login-session could not be cached, an error should be thrown.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", credentialScopes: map[string][]string{}, mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "", sessionCacheError: cacheFailError, expectedError: cacheFailError, }, - {"If the login-session could not be cached, an error should be thrown.", "verifier.org", "https", "https://client.org/callback", "my-super-random-id", "org.fiware.MySpecialCredential", "https://client.org/callback", - "", cacheFailError, cacheFailError, + {testName: "If config service throws an error, no scope should be included.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "myService", credentialScopes: map[string][]string{}, mockConfigError: errors.New("config_error"), expectedCallback: "https://client.org/callback", + expectedConnection: "openid://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, }, } } @@ -198,14 +224,14 @@ func TestStartSameDeviceFlow(t *testing.T) { logging.Configure(true, "DEBUG", true, []string{}) tests := []siopInitTest{ - {"If everything is provided, a samedevice flow should be started.", "myhost.org", "https", "/redirect", "my-random-session-id", "", "https://myhost.org/redirect", - "https://myhost.org/redirect?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://myhost.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", nil, nil, + {testName: "If everything is provided, a samedevice flow should be started.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: map[string][]string{}, mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", + expectedConnection: "https://myhost.org/redirect?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://myhost.org/api/v1/authentication_response&state=randomState&nonce=randomNonce", sessionCacheError: nil, expectedError: nil, }, - {"The scope should be included if configured.", "myhost.org", "https", "/redirect", "my-random-session-id", "org.fiware.MySpecialCredential", "https://myhost.org/redirect", - "https://myhost.org/redirect?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://myhost.org/api/v1/authentication_response&state=randomState&nonce=randomNonce&scope=org.fiware.MySpecialCredential", nil, nil, + {testName: "The scope should be included if configured.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "myService", credentialScopes: map[string][]string{"myService": {"org.fiware.MySpecialCredential"}}, mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", + expectedConnection: "https://myhost.org/redirect?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://myhost.org/api/v1/authentication_response&state=randomState&nonce=randomNonce&scope=org.fiware.MySpecialCredential", sessionCacheError: nil, expectedError: nil, }, - {"If the request cannot be cached, an error should be responded.", "myhost.org", "https", "/redirect", "my-random-session-id", "", "https://myhost.org/redirect", - "", cacheFailError, cacheFailError, + {testName: "If the request cannot be cached, an error should be responded.", testHost: "myhost.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: map[string][]string{}, mockConfigError: nil, expectedCallback: "https://myhost.org/redirect", + expectedConnection: "", sessionCacheError: cacheFailError, expectedError: cacheFailError, }, } @@ -214,8 +240,9 @@ 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 := CredentialVerifier{did: "did:key:verifier", scope: tc.scopeConfig, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator} - authReq, err := verifier.StartSameDeviceFlow(tc.testHost, tc.testProtocol, tc.testSessionId, tc.testAddress) + credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} + verifier := CredentialVerifier{did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, credentialsConfig: credentialsConfig} + authReq, err := verifier.StartSameDeviceFlow(tc.testHost, tc.testProtocol, tc.testSessionId, tc.testAddress, tc.testClientId) verifyInitTest(t, tc, authReq, err, sessionCache, true) }) } @@ -301,26 +328,26 @@ func TestAuthenticationResponse(t *testing.T) { tests := []authTest{ // general behaviour - {"If the credential is invalid, return an error.", true, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, - {"If one credential is invalid, return an error.", true, "login-state", []map[string]interface{}{getVC("vc1"), getVC("vc2")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{true, false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, + {"If the credential is invalid, return an error.", true, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, + {"If one credential is invalid, return an error.", true, "login-state", []map[string]interface{}{getVC("vc1"), getVC("vc2")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true, false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, {"If an authentication response is received without a session, an error should be responded.", true, "", []map[string]interface{}{getVC("vc")}, "holder", loginSession{}, "login-state", nil, []bool{}, nil, SameDeviceResponse{}, nil, ErrorNoSuchSession, nil}, - {"If ssiKit throws an error, an error should be responded.", true, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{}, ssiKitError, SameDeviceResponse{}, nil, ssiKitError, nil}, - {"If tokenCache throws an error, an error should be responded.", true, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, cacheError, cacheError}, - {"If the credential is invalid, return an error.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, - {"If one credential is invalid, return an error.", false, "login-state", []map[string]interface{}{getVC("vc1"), getVC("vc2")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{true, false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, + {"If ssiKit throws an error, an error should be responded.", true, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{}, ssiKitError, SameDeviceResponse{}, nil, ssiKitError, nil}, + {"If tokenCache throws an error, an error should be responded.", true, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, cacheError, cacheError}, + {"If the credential is invalid, return an error.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, + {"If one credential is invalid, return an error.", false, "login-state", []map[string]interface{}{getVC("vc1"), getVC("vc2")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true, false}, nil, SameDeviceResponse{}, nil, ErrorInvalidVC, nil}, {"If an authentication response is received without a session, an error should be responded.", false, "", []map[string]interface{}{getVC("vc")}, "holder", loginSession{}, "login-state", nil, []bool{}, nil, SameDeviceResponse{}, nil, ErrorNoSuchSession, nil}, - {"If ssiKit throws an error, an error should be responded.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{}, ssiKitError, SameDeviceResponse{}, nil, ssiKitError, nil}, - {"If tokenCache throws an error, an error should be responded.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, cacheError, cacheError}, - {"If a non-existent session is requested, an error should be responded.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session"}, "non-existent-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, ErrorNoSuchSession, nil}, + {"If ssiKit throws an error, an error should be responded.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{}, ssiKitError, SameDeviceResponse{}, nil, ssiKitError, nil}, + {"If tokenCache throws an error, an error should be responded.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, cacheError, cacheError}, + {"If a non-existent session is requested, an error should be responded.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "non-existent-state", nil, []bool{true}, nil, SameDeviceResponse{}, nil, ErrorNoSuchSession, nil}, // same-device flow - {"When a same device flow is present, a proper response should be returned.", true, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{"https://myhost.org/callback", "authCode", "my-session"}, nil, nil, nil}, - {"When a same device flow is present, a proper response should be returned for VPs.", true, "login-state", []map[string]interface{}{getVC("vc1"), getVC("vc2")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{true, true}, nil, SameDeviceResponse{"https://myhost.org/callback", "authCode", "my-session"}, nil, nil, nil}, + {"When a same device flow is present, a proper response should be returned.", true, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{"https://myhost.org/callback", "authCode", "my-session"}, nil, nil, nil}, + {"When a same device flow is present, a proper response should be returned for VPs.", true, "login-state", []map[string]interface{}{getVC("vc1"), getVC("vc2")}, "holder", loginSession{true, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true, true}, nil, SameDeviceResponse{"https://myhost.org/callback", "authCode", "my-session"}, nil, nil, nil}, // cross-device flow - {"When a cross-device flow is present, a proper response should be sent to the requestors callback.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, getRequest("https://myhost.org/callback?code=authCode&state=my-session"), nil, nil}, - {"When a cross-device flow is present, a proper response should be sent to the requestors callback for VPs.", false, "login-state", []map[string]interface{}{getVC("vc1"), getVC("vc2")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session"}, "login-state", nil, []bool{true, true}, nil, SameDeviceResponse{}, getRequest("https://myhost.org/callback?code=authCode&state=my-session"), nil, nil}, - {"When the requestor-callback fails, an error should be returned.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session"}, "login-state", callbackError, []bool{true}, nil, SameDeviceResponse{}, nil, callbackError, nil}, + {"When a cross-device flow is present, a proper response should be sent to the requestors callback.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true}, nil, SameDeviceResponse{}, getRequest("https://myhost.org/callback?code=authCode&state=my-session"), nil, nil}, + {"When a cross-device flow is present, a proper response should be sent to the requestors callback for VPs.", false, "login-state", []map[string]interface{}{getVC("vc1"), getVC("vc2")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", nil, []bool{true, true}, nil, SameDeviceResponse{}, getRequest("https://myhost.org/callback?code=authCode&state=my-session"), nil, nil}, + {"When the requestor-callback fails, an error should be returned.", false, "login-state", []map[string]interface{}{getVC("vc")}, "holder", loginSession{false, "https://myhost.org/callback", "my-session", "clientId"}, "login-state", callbackError, []bool{true}, nil, SameDeviceResponse{}, nil, callbackError, nil}, } for _, tc := range tests { @@ -340,7 +367,8 @@ func TestAuthenticationResponse(t *testing.T) { testKey, _ := jwk.New(ecdsKey) jwk.AssignKeyID(testKey) nonceGenerator := mockNonceGenerator{staticValues: []string{"authCode"}} - verifier := CredentialVerifier{did: "did:key:verifier", signingKey: testKey, tokenCache: &tokenCache, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, verificationServices: []VerificationService{&mockExternalSsiKit{tc.verificationResult, tc.verificationError}}, clock: mockClock{}} + credentialsConfig := mockCredentialConfig{} + verifier := CredentialVerifier{did: "did:key:verifier", signingKey: testKey, tokenCache: &tokenCache, sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, verificationServices: []VerificationService{&mockExternalSsiKit{tc.verificationResult, tc.verificationError}}, clock: mockClock{}, credentialsConfig: credentialsConfig} sameDeviceResponse, err := verifier.AuthenticationResponse(tc.requestedState, tc.testVC, tc.testHolder) if err != tc.expectedError { @@ -412,9 +440,9 @@ func TestInitVerifier(t *testing.T) { } tests := []test{ - {"A verifier should be properly intantiated.", configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", SessionExpiry: 30, RequestScope: "org.fiware.MyVC"}, nil}, - {"Without a did, no verifier should be instantiated.", configModel.Verifier{TirAddress: "https://tir.org", SessionExpiry: 30, RequestScope: "org.fiware.MyVC"}, ErrorNoDID}, - {"Without a tir, no verifier should be instantiated.", configModel.Verifier{Did: "did:key:verifier", SessionExpiry: 30, RequestScope: "org.fiware.MyVC"}, ErrorNoTIR}, + {"A verifier should be properly intantiated.", configModel.Verifier{Did: "did:key:verifier", TirAddress: "https://tir.org", SessionExpiry: 30}, nil}, + {"Without a did, no verifier should be instantiated.", configModel.Verifier{TirAddress: "https://tir.org", SessionExpiry: 30}, ErrorNoDID}, + {"Without a tir, no verifier should be instantiated.", configModel.Verifier{Did: "did:key:verifier", SessionExpiry: 30}, ErrorNoTIR}, } for _, tc := range tests { From d1fc0f594d9d83fd5f7ae2de4d0199282da7d7d8 Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 26 May 2023 16:19:08 +0200 Subject: [PATCH 6/8] update the docu --- README.md | 1 + docs/verifier_overview.drawio | 186 +++++++++++++++++++++++++++++++++- docs/verifier_overview.png | Bin 26371 -> 61122 bytes 3 files changed, 186 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 811c5d97..9c017d6b 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ The following actions occur in the interaction: 1. The user opens the frontend application. 2. The frontend-application forwards the user to the login-page of VCVerifier 3. The VCVerifier presents a QR-code, containing the ```openid:```-connection string with all necessary information to start the authentication process. The QR-code is scanned by the user's wallet. + 1. the Verifier retrieves the Scope-Information from the Config-Service 4. The user approves the wallet's interaction with the VCVerifier and the VerifiableCredential is presented via the OIDC4VP-flow. 5. VCVerifier verifies the credential: 1. at WaltID-SSIKit with the configured set of policies diff --git a/docs/verifier_overview.drawio b/docs/verifier_overview.drawio index 59cebe93..b85d9029 100644 --- a/docs/verifier_overview.drawio +++ b/docs/verifier_overview.drawio @@ -1 +1,185 @@ -3Vpbk6I4FP41PtoFhAA+aju9MzW9u1Nl1cz0Iw1RsxONE6Kt/es3QIKEiDiKl/bJ5JAcwpfznUtiBzzO1n+xcDH9m8aIdBwrXnfAsOM4tt2D4ieVbHKJB6xcMGE4loO2ghF+R1Kohi1xjBJtIKeUcLzQhRGdz1HENVnIGH3Th40p0d+6CCfIEIyikJjSHzjm01waQGsr/4zwZKrebFvyySxUg6UgmYYxfSuJwKcOeGSU8rw1Wz8ikoKncMnnPdU8LRbG0JwfMuH95/C/z/8kz3139W/y1H+x8LrXlVpWIVnKD5aL5RuFAKPLeYxSJVYHDN6mmKPRIozSp29iz4VsymdE9GzRlOoQ42hdu067+HphNojOEGcbMUROAApBaTG2L/tvW/xBT8qmJeyBsplQ7vmk0L2FRTQkMn+CkrUDJo+I9w5ivErfSPBknj3wfi/TDR0QNObbnmhN0t/+kk8pw+8hx3TefQ43iClFYmGZrnygpl4XvbKqpMWptXtvN++9AiH7dDAYY0IeKaEsUwXGQYSiSMgTzugvVHryGkAXWu0Yjwt04ynYVzIeB+wwHu9ctuMYpjMIo19oHndHiK2wwPIk0FvADLrNmNnOJTFzDcyeGJ3zFLT+YkFwlPHn6sAVQN0McNAA7vvjd8TwGKeO5sbgAsG14fIMuH6EhH8ZdkejL18xvzpirndriNlmwtCPODWNS2Q7i7S5nJF8ABikUAjqkufwFZFvNMEZicHwlXJOZ6UBfRlIOK1ASpec4LmIHSrXaytqNPP4ojHDNoOGMEyCrm+RB1DYvyhSwEDKwEiEjX5aCYheRMIkwZEOi57gCiDY5me585J2HqDqDtflh8ONZoYoNsqJCrSifgnZBPGm8GduQQljuANjJWOIiPC40pexC3j5hm8UiwUWO+xXuAArW5fQJYuQnFQuNyp67Koiq6Ioh8FQlJlB8dUn5F2mZdimaYjtytzR7kw2EluH2A7PNMNxnOoYMJSIbP4105dawSL9nOwD4aADh/tIJytWOblTUKtsMXtsvpaiXevB9gHQwFdZ6LG2oYbQ8ThBZ9ku28z5TmbyGvOMyA8OlN0XOTBtb3mcdhSNt/SXswoP0Br/cwY1p7+NfgJe009And7QOdZPVMp917+sn7DNnPkwu0vEwvhNBZZGw1J5xW1bll1JMiBoKQS5wYVDkOnTwJ2EoBoDkepFBAIB1LFvxTSUa9SVXiA6mZXiicHpcFY3kjVn9dXigL7PxiHsoWTtVk/uehcmqxkH3Dshq7eXrCJfBLZbyRft02zjAoz022ZkKfPTEj+7IT7X5AGZ+AkTvfj++PWhE9QcAfwp4R3QkEGemfDAPMry7oTwfhPhnUCD/vbZHhzJ9ksl6arWVMVlaVZtqXm4R2hM7OGBnqMmElzIc1QqPe/YVMGoPavHi+f2HOZ1MLwTzxHs9Rwir9ezvQ+QJ/RazxOuS/WrBn9QCf72saW5cU9fPWY+d7ZvmoV/JxTuNQV/6LXkzvUZ5+OwY2ZqLXH42KNh/1pHwypk3vYJHqgcvIFjL5Hc6n3jgZdIwhTCTWmYJF7tgt1AT4gBsPauqzjl2D1eNPIVtJt2OAYNgvvwWQ2XzsJnWT7Qj/5uPvFQ6/1IN9PtuR/nqu6nys5jjyhA9f831XTn3IWGeSbZuw/Gg5o0tig0HOh7GvanXmHvvkFQWk/xB53034nqX9z58O1/4cGn/wE= \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/verifier_overview.png b/docs/verifier_overview.png index d5663325f886a413c2b3a2b185d7f83a49b99657..d3f99f1e5f85301d83d6ccb0a1d77f01c10c8bbf 100644 GIT binary patch literal 61122 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≥(_4T2m6g(C03fnv{mguB$;zS$#q3H67_qafjL? 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 literal 26371 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 From acfa188edd280ef2002e707e4203180399788957 Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 26 May 2023 16:44:21 +0200 Subject: [PATCH 7/8] logging --- verifier/verifier.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/verifier/verifier.go b/verifier/verifier.go index 16755ccc..fbf9a9da 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -426,8 +426,9 @@ func (v *CredentialVerifier) getTrustRegistriesVerificationContext(clientId stri trustedIssuersLists = append(trustedIssuersLists, issuersLists...) trustedParticipantsRegistries = append(trustedParticipantsRegistries, participantsLists...) } - - return TrustRegistriesVerificationContext{trustedIssuersLists: trustedIssuersLists, trustedParticipantsRegistries: trustedParticipantsRegistries}, err + context := TrustRegistriesVerificationContext{trustedIssuersLists: trustedIssuersLists, trustedParticipantsRegistries: trustedParticipantsRegistries} + logging.Log().Debugf("The verification context is: %s", logging.PrettyPrintObject(context)) + return context, err } // TODO Use more generic approach to validate that every credential is issued by a party that we trust From 6a4b03e591c69523bd514c6cad117c707f26c20f Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Tue, 30 May 2023 14:52:40 +0200 Subject: [PATCH 8/8] fix caching --- go.mod | 1 + go.sum | 2 ++ tir/tirClient.go | 6 +++-- verifier/credentialsConfig.go | 47 +++++++++++++++++++++++++++------- verifier/trustedissuer.go | 6 +++++ verifier/trustedparticipant.go | 1 + verifier/verifier.go | 1 - 7 files changed, 52 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 1d380a5e..2273f544 100644 --- a/go.mod +++ b/go.mod @@ -65,6 +65,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.7 // indirect github.com/penglongli/gin-metrics v0.1.10 + github.com/procyon-projects/chrono v1.1.2 github.com/sirupsen/logrus v1.9.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/twitchyliquid64/golang-asm v0.15.1 // indirect diff --git a/go.sum b/go.sum index ad5cf2a4..14f1a0fc 100644 --- a/go.sum +++ b/go.sum @@ -286,6 +286,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/procyon-projects/chrono v1.1.2 h1:Uw7V96Ckl/pOeMBNvaEki7k6Ssgd9OX8b9PY0gpXmoU= +github.com/procyon-projects/chrono v1.1.2/go.mod h1:RwQ27W7hRaq+QUWN2yXU3BDG2FUyEQiKds8/M1FI5C8= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= diff --git a/tir/tirClient.go b/tir/tirClient.go index abd773f3..fcfbac80 100644 --- a/tir/tirClient.go +++ b/tir/tirClient.go @@ -85,7 +85,7 @@ func (tc TirHttpClient) IsTrustedParticipant(tirEndpoints []string, did string) for _, tirEndpoint := range tirEndpoints { logging.Log().Debugf("Check if a participant %s is trusted through %s.", did, tirEndpoint) - if tc.issuerExists(getIssuerUrl(tirEndpoint), did) { + if tc.issuerExists(tirEndpoint, did) { logging.Log().Debugf("Issuer %s is a trusted participant via %s.", did, tirEndpoint) return true } @@ -109,6 +109,7 @@ func (tc TirHttpClient) GetTrustedIssuer(tirEndpoints []string, did string) (exi logging.Log().Warnf("Was not able to parse the response from tir %s for %s. Err: %v", tirEndpoint, did, err) continue } + logging.Log().Debugf("Got issuer %s.", logging.PrettyPrintObject(trustedIssuer)) return true, trustedIssuer, err } return false, trustedIssuer, err @@ -134,12 +135,13 @@ func (tc TirHttpClient) issuerExists(tirEndpoint string, did string) (trusted bo if err != nil { return false } + logging.Log().Debugf("Issuer %s response from %s is %v", did, tirEndpoint, resp.StatusCode) // if a 200 is returned, the issuer exists. We dont have to parse the whole response return resp.StatusCode == 200 } func (tc TirHttpClient) requestIssuer(tirEndpoint string, did string) (response *http.Response, err error) { - resp, err := tc.client.Get(tirEndpoint + "/" + did) + resp, err := tc.client.Get(getIssuerUrl(tirEndpoint) + "/" + did) if err != nil { logging.Log().Warnf("Was not able to get the issuer %s from %s. Err: %v", did, tirEndpoint, err) return resp, err diff --git a/verifier/credentialsConfig.go b/verifier/credentialsConfig.go index 7834c9df..0657089d 100644 --- a/verifier/credentialsConfig.go +++ b/verifier/credentialsConfig.go @@ -1,6 +1,7 @@ package verifier import ( + "context" "fmt" "net/url" "time" @@ -8,9 +9,10 @@ import ( "github.com/fiware/VCVerifier/config" "github.com/fiware/VCVerifier/logging" "github.com/patrickmn/go-cache" + "github.com/procyon-projects/chrono" ) -const CACHE_EXPIRY = 30 +const CACHE_EXPIRY = 60 const CACHE_KEY_TEMPLATE = "%s-%s" /** @@ -27,6 +29,7 @@ type CredentialsConfig interface { } type ServiceBackedCredentialsConfig struct { + initialConfig *config.ConfigRepo configEndpoint *url.URL scopeCache Cache trustedParticipantsCache Cache @@ -42,41 +45,67 @@ func InitServiceBackedCredentialsConfig(repoConfig *config.ConfigRepo) (credenti logging.Log().Errorf("The service endpoint %s is not a valid url. Err: %v", repoConfig.ConfigEndpoint, err) return } - scopeCache := cache.New(time.Duration(CACHE_EXPIRY)*time.Second, time.Duration(2*CACHE_EXPIRY)*time.Second) - trustedParticipantsCache := cache.New(time.Duration(CACHE_EXPIRY)*time.Second, time.Duration(2*CACHE_EXPIRY)*time.Second) - trustedIssuersCache := cache.New(time.Duration(CACHE_EXPIRY)*time.Second, time.Duration(2*CACHE_EXPIRY)*time.Second) - for serviceId, serviceConfig := range repoConfig.Services { - scopeCache.Add(serviceId, serviceConfig.Scope, cache.DefaultExpiration) + var scopeCache Cache = cache.New(CACHE_EXPIRY*time.Second, 2*CACHE_EXPIRY*time.Second) + var trustedParticipantsCache Cache = cache.New(CACHE_EXPIRY*time.Second, 2*CACHE_EXPIRY*time.Second) + var trustedIssuersCache Cache = cache.New(CACHE_EXPIRY*time.Second, 2*CACHE_EXPIRY*time.Second) + + scb := ServiceBackedCredentialsConfig{configEndpoint: serviceUrl, scopeCache: scopeCache, trustedParticipantsCache: trustedParticipantsCache, trustedIssuersCache: trustedIssuersCache, initialConfig: repoConfig} + scb.fillStaticValues() + taskScheduler := chrono.NewDefaultTaskScheduler() + taskScheduler.ScheduleAtFixedRate(scb.fillCache, time.Duration(30)*time.Second) + + return scb, err +} + +func (cc ServiceBackedCredentialsConfig) fillStaticValues() { + for serviceId, serviceConfig := range cc.initialConfig.Services { + logging.Log().Debugf("Add to scope cache: %s", serviceId) + cc.scopeCache.Add(serviceId, serviceConfig.Scope, cache.NoExpiration) for vcType, trustedParticipants := range serviceConfig.TrustedParticipants { - trustedParticipantsCache.Add(fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceId, vcType), trustedParticipants, cache.DefaultExpiration) + logging.Log().Debugf("Add to trusted participants cache: %s", fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceId, vcType)) + cc.trustedParticipantsCache.Add(fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceId, vcType), trustedParticipants, cache.NoExpiration) + } for vcType, trustedIssuers := range serviceConfig.TrustedIssuers { - trustedIssuersCache.Add(fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceId, vcType), trustedIssuers, cache.DefaultExpiration) + logging.Log().Debugf("Add to trusted issuers cache: %s", fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceId, vcType)) + cc.trustedIssuersCache.Add(fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceId, vcType), trustedIssuers, cache.NoExpiration) } } - return ServiceBackedCredentialsConfig{configEndpoint: serviceUrl, scopeCache: scopeCache, trustedParticipantsCache: trustedParticipantsCache, trustedIssuersCache: trustedIssuersCache}, err +} + +func (cc ServiceBackedCredentialsConfig) fillCache(ctx context.Context) { + + // TODO: add fill from service } func (cc ServiceBackedCredentialsConfig) GetScope(serviceIdentifier string) (credentialTypes []string, err error) { cacheEntry, hit := cc.scopeCache.Get(serviceIdentifier) if hit { + logging.Log().Debugf("Found scope for %s", serviceIdentifier) return cacheEntry.([]string), nil } + logging.Log().Debugf("No scope entry for %s", serviceIdentifier) return []string{}, nil } func (cc ServiceBackedCredentialsConfig) GetTrustedParticipantLists(serviceIdentifier string, credentialType string) (trustedIssuersRegistryUrl []string, err error) { + logging.Log().Debugf("Get participants list for %s.", fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceIdentifier, credentialType)) cacheEntry, hit := cc.trustedParticipantsCache.Get(fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceIdentifier, credentialType)) if hit { + logging.Log().Debugf("Found trusted participants %s for %s - %s", cacheEntry.([]string), serviceIdentifier, credentialType) return cacheEntry.([]string), nil } + logging.Log().Debugf("No trusted participants for %s - %s", serviceIdentifier, credentialType) return []string{}, nil } func (cc ServiceBackedCredentialsConfig) GetTrustedIssuersLists(serviceIdentifier string, credentialType string) (trustedIssuersRegistryUrl []string, err error) { + logging.Log().Debugf("Get issuers list for %s.", fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceIdentifier, credentialType)) cacheEntry, hit := cc.trustedIssuersCache.Get(fmt.Sprintf(CACHE_KEY_TEMPLATE, serviceIdentifier, credentialType)) if hit { + logging.Log().Debugf("Found trusted issuers for %s - %s", serviceIdentifier, credentialType) return cacheEntry.([]string), nil } + logging.Log().Debugf("No trusted issuers for %s - %s", serviceIdentifier, credentialType) return []string{}, nil } diff --git a/verifier/trustedissuer.go b/verifier/trustedissuer.go index c8274dcc..fc6b6516 100644 --- a/verifier/trustedissuer.go +++ b/verifier/trustedissuer.go @@ -17,6 +17,8 @@ type TrustedIssuerVerificationService struct { } func (tpvs *TrustedIssuerVerificationService) VerifyVC(verifiableCredential VerifiableCredential, verificationContext VerificationContext) (result bool, err error) { + + logging.Log().Debugf("Verify trusted issuer for %s", logging.PrettyPrintObject(verifiableCredential)) defer func() { if recErr := recover(); recErr != nil { logging.Log().Warnf("Was not able to convert context. Err: %v", recErr) @@ -27,9 +29,11 @@ func (tpvs *TrustedIssuerVerificationService) VerifyVC(verifiableCredential Veri exist, trustedIssuer, err := tpvs.tirClient.GetTrustedIssuer(trustContext.GetTrustedParticipantLists(), verifiableCredential.Issuer) if err != nil { + logging.Log().Warnf("Was not able to verify trusted issuer. Err: %v", err) return false, err } if !exist { + logging.Log().Warnf("Trusted issuer for %s does not exist in context %s.", logging.PrettyPrintObject(verifiableCredential), logging.PrettyPrintObject(verificationContext)) return false, err } credentials, err := parseAttributes(trustedIssuer) @@ -107,6 +111,8 @@ func contains(interfaces []interface{}, interfaceToCheck interface{}) bool { return true } } + logging.Log().Debugf("%s does not contain %s", logging.PrettyPrintObject(interfaces), logging.PrettyPrintObject(interfaceToCheck)) + return false } diff --git a/verifier/trustedparticipant.go b/verifier/trustedparticipant.go index c3c5e428..d0711b3f 100644 --- a/verifier/trustedparticipant.go +++ b/verifier/trustedparticipant.go @@ -18,6 +18,7 @@ type TrustedParticipantVerificationService struct { func (tpvs *TrustedParticipantVerificationService) VerifyVC(verifiableCredential VerifiableCredential, verificationContext VerificationContext) (result bool, err error) { + logging.Log().Debugf("Verify trusted participant for %s", logging.PrettyPrintObject(verifiableCredential)) defer func() { if recErr := recover(); recErr != nil { logging.Log().Warnf("Was not able to convert context. Err: %v", recErr) diff --git a/verifier/verifier.go b/verifier/verifier.go index fbf9a9da..4f030d24 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -427,7 +427,6 @@ func (v *CredentialVerifier) getTrustRegistriesVerificationContext(clientId stri trustedParticipantsRegistries = append(trustedParticipantsRegistries, participantsLists...) } context := TrustRegistriesVerificationContext{trustedIssuersLists: trustedIssuersLists, trustedParticipantsRegistries: trustedParticipantsRegistries} - logging.Log().Debugf("The verification context is: %s", logging.PrettyPrintObject(context)) return context, err }