diff --git a/README.md b/README.md index 46a3ae9f..9c017d6b 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,13 @@ 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 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 +116,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 2ba40eb5..1a226d00 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 @@ -46,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"` } @@ -59,6 +58,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..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": {}, @@ -53,6 +52,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, }, { @@ -66,7 +79,6 @@ func Test_ReadConfig(t *testing.T) { Verifier: Verifier{Did: "", TirAddress: "", SessionExpiry: 30, - RequestScope: "", }, SSIKit: SSIKit{ AuditorURL: "", }, 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 d5663325..d3f99f1e 100644 Binary files a/docs/verifier_overview.png and b/docs/verifier_overview.png differ 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..2273f544 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 @@ -63,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 ef34f83d..14f1a0fc 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= @@ -281,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/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/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/tir/tirClient.go b/tir/tirClient.go new file mode 100644 index 00000000..fcfbac80 --- /dev/null +++ b/tir/tirClient.go @@ -0,0 +1,162 @@ +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) +} + +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 TirHttpClient 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"` +} + +/** +* 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) + if err != nil { + logging.Log().Errorf("Was not able to inject the cach to the client. Err: %v", err) + return + } + return TirHttpClient{httpClient}, err +} + +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) + if tc.issuerExists(tirEndpoint, did) { + logging.Log().Debugf("Issuer %s is a trusted participant via %s.", did, tirEndpoint) + return true + } + } + return false +} + +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 { + 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 + } + logging.Log().Debugf("Got issuer %s.", logging.PrettyPrintObject(trustedIssuer)) + 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 TirHttpClient) issuerExists(tirEndpoint string, did string) (trusted bool) { + resp, err := tc.requestIssuer(tirEndpoint, did) + 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(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 + } + 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..0657089d --- /dev/null +++ b/verifier/credentialsConfig.go @@ -0,0 +1,111 @@ +package verifier + +import ( + "context" + "fmt" + "net/url" + "time" + + "github.com/fiware/VCVerifier/config" + "github.com/fiware/VCVerifier/logging" + "github.com/patrickmn/go-cache" + "github.com/procyon-projects/chrono" +) + +const CACHE_EXPIRY = 60 +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 { + initialConfig *config.ConfigRepo + 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 + } + 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 { + 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 { + 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) + } + } +} + +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/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..fc6b6516 --- /dev/null +++ b/verifier/trustedissuer.go @@ -0,0 +1,143 @@ +package verifier + +import ( + "encoding/base64" + "encoding/json" + + "github.com/fiware/VCVerifier/logging" + tir "github.com/fiware/VCVerifier/tir" + "golang.org/x/exp/slices" +) + +/** +* 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) { + + 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) + err = ErrorCannotConverContext + } + }() + trustContext := verificationContext.(TrustRegistriesVerificationContext) + 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) + 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 + } + } + logging.Log().Debugf("%s does not contain %s", logging.PrettyPrintObject(interfaces), logging.PrettyPrintObject(interfaceToCheck)) + + 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..996a15aa --- /dev/null +++ b/verifier/trustedissuer_test.go @@ -0,0 +1,136 @@ +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 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: 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}, + {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 new file mode 100644 index 00000000..d0711b3f --- /dev/null +++ b/verifier/trustedparticipant.go @@ -0,0 +1,34 @@ +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. + */ +type TrustedParticipantVerificationService struct { + tirClient tir.TirClient +} + +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) + err = ErrorCannotConverContext + } + }() + trustContext := verificationContext.(TrustRegistriesVerificationContext) + 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 + } + 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 4296a0c0..4f030d24 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -10,9 +10,11 @@ import ( "io" "net/http" "net/url" + "strings" "time" configModel "github.com/fiware/VCVerifier/config" + "github.com/fiware/VCVerifier/tir" logging "github.com/fiware/VCVerifier/logging" @@ -29,28 +31,29 @@ 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) } -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. @@ -59,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 @@ -73,8 +74,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 +88,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 { @@ -133,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 @@ -164,7 +178,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,14 +188,28 @@ 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) + } + + 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 @@ -190,16 +218,18 @@ func InitVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIK verifier = &CredentialVerifier{ verifierConfig.Did, verifierConfig.TirAddress, - verifierConfig.RequestScope, key, sessionCache, tokenCache, &randomGenerator{}, realClock{}, jwtTokenSigner{}, - []ExternalVerificationService{ + credentialsConfig, + []VerificationService{ &externalSsiKitVerifier, &externalGaiaXVerifier, + &trustedParticipantVerificationService, + &trustedIssuerVerificationService, }, } @@ -210,10 +240,10 @@ func InitVerifier(verifierConfig *configModel.Verifier, ssiKitClient ssikit.SSIK /** * Initializes the cross-device login flow and returns all neccessary information as a qr-code **/ -func (v *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 @@ -229,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) @@ -252,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 } /** @@ -323,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) + 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 @@ -373,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...) + } + context := TrustRegistriesVerificationContext{trustedIssuersLists: trustedIssuersLists, trustedParticipantsRegistries: trustedParticipantsRegistries} + return context, 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 { @@ -408,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 { @@ -419,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 @@ -429,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 { @@ -448,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" + @@ -458,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 104de224..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 @@ -125,13 +146,15 @@ 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"}} + 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) + }) } } @@ -142,13 +165,15 @@ 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"}} + 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) + }) } } @@ -167,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) } @@ -178,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, }, } } @@ -196,25 +224,27 @@ 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, }, } 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"}} + 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) + }) } } @@ -224,7 +254,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 } @@ -298,63 +328,65 @@ 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 { + 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"}} + 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} - 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: []ExternalVerificationService{&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 { + return + } - 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 - } + if tc.sameDevice { + verifySameDevice(t, sameDeviceResponse, tokenCache, tc) + return + } - if tc.sameDevice { - verifySameDevice(t, sameDeviceResponse, tokenCache, tc) - continue - } - - 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) + } + }) } } @@ -408,31 +440,32 @@ 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 { + 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, &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 +539,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 + } + }) } }