diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vci.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vci.go index 58f6ad013..1a2a323fe 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vci.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vci.go @@ -55,10 +55,19 @@ type OIDC4VCIConfig struct { EnableDiscoverableClientID bool } +type credentialRequestOpts struct { + signerKeyID string + signature string + nonce string +} + type OauthClientOpt func(config *oauth2.Config) +type CredentialRequestOpt func(credentialRequestOpts *credentialRequestOpts) + type Hooks struct { - BeforeTokenRequest []OauthClientOpt + BeforeTokenRequest []OauthClientOpt + BeforeCredentialRequest []CredentialRequestOpt } func WithClientID(clientID string) OauthClientOpt { @@ -67,6 +76,27 @@ func WithClientID(clientID string) OauthClientOpt { } } +// WithSignerKeyID overrides signerKeyID in credentials request. For testing purpose only. +func WithSignerKeyID(keyID string) CredentialRequestOpt { + return func(credentialRequestOpts *credentialRequestOpts) { + credentialRequestOpts.signerKeyID = keyID + } +} + +// WithSignatureValue overrides signature in credentials request. For testing purpose only. +func WithSignatureValue(signature string) CredentialRequestOpt { + return func(credentialRequestOpts *credentialRequestOpts) { + credentialRequestOpts.signature = signature + } +} + +// WithNonce overrides nonce in credentials request. For testing purpose only. +func WithNonce(nonce string) CredentialRequestOpt { + return func(credentialRequestOpts *credentialRequestOpts) { + credentialRequestOpts.nonce = nonce + } +} + func (s *Service) RunOIDC4VCI(config *OIDC4VCIConfig, hooks *Hooks) error { log.Println("Starting OIDC4VCI authorized code flow") log.Printf("Credential Offer URI:\n\n\t%s\n\n", config.CredentialOfferURI) @@ -178,9 +208,11 @@ func (s *Service) RunOIDC4VCI(config *OIDC4VCIConfig, hooks *Hooks) error { ctx = context.WithValue(ctx, oauth2.HTTPClient, s.httpClient) var beforeTokenRequestHooks []OauthClientOpt + var beforeCredentialsRequestHooks []CredentialRequestOpt if hooks != nil { beforeTokenRequestHooks = hooks.BeforeTokenRequest + beforeCredentialsRequestHooks = hooks.BeforeCredentialRequest } for _, f := range beforeTokenRequestHooks { @@ -205,6 +237,7 @@ func (s *Service) RunOIDC4VCI(config *OIDC4VCIConfig, hooks *Hooks) error { config.CredentialType, config.CredentialFormat, credentialOfferResponse.CredentialIssuer, + beforeCredentialsRequestHooks..., ) if err != nil { return fmt.Errorf("get credential: %w", err) @@ -337,9 +370,11 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4VCIConfig, hooks *Hooks ctx = context.WithValue(ctx, oauth2.HTTPClient, s.httpClient) var beforeTokenRequestHooks []OauthClientOpt + var beforeCredentialsRequestHooks []CredentialRequestOpt if hooks != nil { beforeTokenRequestHooks = hooks.BeforeTokenRequest + beforeCredentialsRequestHooks = hooks.BeforeCredentialRequest } for _, f := range beforeTokenRequestHooks { @@ -362,6 +397,7 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4VCIConfig, hooks *Hooks config.CredentialType, config.CredentialFormat, issuerUrl, + beforeCredentialsRequestHooks..., ) if err != nil { return fmt.Errorf("get credential: %w", err) @@ -483,7 +519,13 @@ func (s *Service) getCredential( credentialType, credentialFormat, issuerURI string, + beforeCredentialRequestOpts ...CredentialRequestOpt, ) (interface{}, time.Duration, error) { + credentialsRequestParamsOverride := &credentialRequestOpts{} + for _, f := range beforeCredentialRequestOpts { + f(credentialsRequestParamsOverride) + } + didKeyID := s.vcProviderConf.WalletParams.DidKeyID[0] fks, err := s.ariesServices.Suite().FixedKeyMultiSigner(strings.Split(didKeyID, "#")[1]) @@ -493,11 +535,16 @@ func (s *Service) getCredential( kmsSigner := signer.NewKMSSigner(fks, s.vcProviderConf.WalletParams.SignType, nil) + nonce := s.token.Extra("c_nonce").(string) + if credentialsRequestParamsOverride.nonce != "" { + nonce = credentialsRequestParamsOverride.nonce + } + claims := &JWTProofClaims{ Issuer: s.oauthClient.ClientID, IssuedAt: time.Now().Unix(), Audience: issuerURI, - Nonce: s.token.Extra("c_nonce").(string), + Nonce: nonce, } signerKeyID := didKeyID @@ -518,6 +565,10 @@ func (s *Service) getCredential( signerKeyID = res.DIDDocument.VerificationMethod[0].ID } + if credentialsRequestParamsOverride.signerKeyID != "" { + signerKeyID = credentialsRequestParamsOverride.signerKeyID + } + headers := map[string]interface{}{ jose.HeaderType: jwtProofTypHeader, } @@ -533,6 +584,11 @@ func (s *Service) getCredential( return nil, 0, fmt.Errorf("serialize signed jwt: %w", err) } + if credentialsRequestParamsOverride.signature != "" { + chunks := strings.Split(jws, ".") + jws = strings.Join([]string{chunks[0], chunks[1], credentialsRequestParamsOverride.signature}, ".") + } + b, err := json.Marshal(CredentialRequest{ Format: credentialFormat, Types: []string{"VerifiableCredential", credentialType}, diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vci_pre_auth.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vci_pre_auth.go index be64d21ea..f4c71bb89 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vci_pre_auth.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4vci_pre_auth.go @@ -25,7 +25,7 @@ import ( "github.com/trustbloc/vcs/pkg/restapi/v1/oidc4ci" ) -func (s *Service) RunOIDC4CIPreAuth(config *OIDC4VCIConfig) (*verifiable.Credential, error) { +func (s *Service) RunOIDC4CIPreAuth(config *OIDC4VCIConfig, hooks *Hooks) (*verifiable.Credential, error) { log.Println("Starting OIDC4VCI pre-authorized code flow") startTime := time.Now() @@ -105,13 +105,21 @@ func (s *Service) RunOIDC4CIPreAuth(config *OIDC4VCIConfig) (*verifiable.Credent "c_nonce": *token.CNonce, }) + var beforeCredentialsRequestHooks []CredentialRequestOpt + + if hooks != nil { + beforeCredentialsRequestHooks = hooks.BeforeCredentialRequest + } + s.print("Getting credential") startTime = time.Now() vc, vcsDuration, err := s.getCredential( oidcIssuerCredentialConfig.CredentialEndpoint, config.CredentialType, config.CredentialFormat, - credentialOfferResponse.CredentialIssuer) + credentialOfferResponse.CredentialIssuer, + beforeCredentialsRequestHooks..., + ) if err != nil { return nil, fmt.Errorf("get credential: %w", err) } diff --git a/test/bdd/features/oidc4vc_api.feature b/test/bdd/features/oidc4vc_api.feature index adf7da76a..c5c319b11 100644 --- a/test/bdd/features/oidc4vc_api.feature +++ b/test/bdd/features/oidc4vc_api.feature @@ -118,6 +118,21 @@ Feature: OIDC4VC REST API And User holds credential "UniversityDegreeCredential" with templateID "universityDegreeTemplateID" Then Malicious attacker stealing auth code from User and using "malicious_attacker_id" ClientID makes /token request and receives "invalid_client" error + Scenario: OIDC credential issuance and verification Auth flow (Malicious attacker changed signingKeyID & calling credential endpoint with it) + Given Profile "bank_issuer/v1.0" issuer has been authorized with username "profile-user-issuer-1" and password "profile-user-issuer-1-pwd" + And User holds credential "UniversityDegreeCredential" with templateID "universityDegreeTemplateID" + Then Malicious attacker changed JWT kid header and makes /credential request and receives "invalid_or_missing_proof" error + + Scenario: OIDC credential issuance and verification Auth flow (Malicious attacker changed JWT signature value & calling credential endpoint with it) + Given Profile "bank_issuer/v1.0" issuer has been authorized with username "profile-user-issuer-1" and password "profile-user-issuer-1-pwd" + And User holds credential "UniversityDegreeCredential" with templateID "universityDegreeTemplateID" + Then Malicious attacker changed signature value and makes /credential request and receives "invalid_or_missing_proof" error + + Scenario: OIDC credential issuance and verification Auth flow (Malicious attacker changed nonce & calling credential endpoint with it) + Given Profile "bank_issuer/v1.0" issuer has been authorized with username "profile-user-issuer-1" and password "profile-user-issuer-1-pwd" + And User holds credential "UniversityDegreeCredential" with templateID "universityDegreeTemplateID" + Then Malicious attacker changed nonce value and makes /credential request and receives "invalid_or_missing_proof" error + Scenario: OIDC credential issuance and verification Pre Auth flow (issuer has pre-authorized_grant_anonymous_access_supported disabled) Given Profile "i_disabled_preauth_without_client_id/v1.0" issuer has been authorized with username "profile-user-issuer-1" and password "profile-user-issuer-1-pwd" And User holds credential "VerifiedEmployee" with templateID "templateID" diff --git a/test/bdd/pkg/v1/oidc4vc/oidc4ci.go b/test/bdd/pkg/v1/oidc4vc/oidc4ci.go index 27570422b..78ee32b7d 100644 --- a/test/bdd/pkg/v1/oidc4vc/oidc4ci.go +++ b/test/bdd/pkg/v1/oidc4vc/oidc4ci.go @@ -128,7 +128,7 @@ func (s *Steps) runOIDC4CIPreAuth(initiateOIDC4CIRequest initiateOIDC4CIRequest) CredentialType: s.issuedCredentialType, CredentialFormat: s.issuerProfile.CredentialMetaData.CredentialsSupported[0]["format"].(string), Pin: *initiateOIDC4CIResponseData.UserPin, - }) + }, nil) if err != nil { return fmt.Errorf("s.walletRunner.RunOIDC4CIPreAuth: %w", err) } @@ -250,7 +250,7 @@ func (s *Steps) credentialTypeTemplateID(issuedCredentialType, issuedCredentialT return nil } -func (s *Steps) runOIDC4CIAuthWithError(updatedClientID, errorContains string) error { +func (s *Steps) runOIDC4CIAuthWithErrorInvalidClient(updatedClientID, errorContains string) error { initiateOIDC4CIResponseData, err := s.initiateCredentialIssuance(s.getInitiateIssuanceRequest()) if err != nil { return fmt.Errorf("initiateCredentialIssuance: %w", err) @@ -301,6 +301,63 @@ func (s *Steps) runOIDC4CIAuthWithError(updatedClientID, errorContains string) e return nil } +func (s *Steps) runOIDC4CIAuthWithErrorInvalidSigningKeyID(errorContains string) error { + return s.runOIDC4CIAuthWithErrorInvalidSignature( + []walletrunner.CredentialRequestOpt{ + walletrunner.WithSignerKeyID("didID#keyID"), + }, + errorContains, + ) +} + +func (s *Steps) runOIDC4CIAuthWithErrorInvalidSignatureValue(errorContains string) error { + return s.runOIDC4CIAuthWithErrorInvalidSignature( + []walletrunner.CredentialRequestOpt{ + walletrunner.WithSignatureValue(uuid.NewString()), + }, + errorContains, + ) +} + +func (s *Steps) runOIDC4CIAuthWithErrorInvalidNonce(errorContains string) error { + return s.runOIDC4CIAuthWithErrorInvalidSignature( + []walletrunner.CredentialRequestOpt{ + walletrunner.WithNonce(uuid.NewString()), + }, + errorContains, + ) +} + +func (s *Steps) runOIDC4CIAuthWithErrorInvalidSignature(beforeCredentialRequestOpts []walletrunner.CredentialRequestOpt, errorContains string) error { + initiateOIDC4CIResponseData, err := s.initiateCredentialIssuance(s.getInitiateIssuanceRequest()) + if err != nil { + return fmt.Errorf("initiateCredentialIssuance: %w", err) + } + + err = s.walletRunner.RunOIDC4VCI(&walletrunner.OIDC4VCIConfig{ + CredentialOfferURI: initiateOIDC4CIResponseData.OfferCredentialURL, + ClientID: "oidc4vc_client", + Scopes: []string{"openid", "profile"}, + RedirectURI: "http://127.0.0.1/callback", + CredentialType: s.issuedCredentialType, + CredentialFormat: s.issuerProfile.CredentialMetaData.CredentialsSupported[0]["format"].(string), + Login: "bdd-test", + Password: "bdd-test-pass", + }, &walletrunner.Hooks{ + BeforeCredentialRequest: beforeCredentialRequestOpts, + }) + + if err == nil { + return fmt.Errorf("error expected, got nil") + } + + if !strings.Contains(err.Error(), errorContains) { + return fmt.Errorf("unexpected err: %w", err) + } + + return nil +} + func (s *Steps) runOIDC4CIAuth() error { initiateOIDC4CIResponseData, err := s.initiateCredentialIssuance(s.getInitiateIssuanceRequest()) if err != nil { diff --git a/test/bdd/pkg/v1/oidc4vc/steps.go b/test/bdd/pkg/v1/oidc4vc/steps.go index 5b4f57143..9b0608eee 100644 --- a/test/bdd/pkg/v1/oidc4vc/steps.go +++ b/test/bdd/pkg/v1/oidc4vc/steps.go @@ -139,7 +139,10 @@ func (s *Steps) RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^Verifier with profile "([^"]*)" requests expired interactions claims$`, s.retrieveExpiredOrDeletedInteractionsClaim) sc.Step(`^Verifier with profile "([^"]*)" waits for interaction succeeded event$`, s.waitForOIDCInteractionSucceededEvent) sc.Step(`^User interacts with Verifier and initiate OIDC4VP interaction under "([^"]*)" profile with presentation definition ID "([^"]*)" and fields "([^"]*)" and receives "([^"]*)" error$`, s.runOIDC4VPFlowWithError) - sc.Step(`^Malicious attacker stealing auth code from User and using "([^"]*)" ClientID makes /token request and receives "([^"]*)" error$`, s.runOIDC4CIAuthWithError) + sc.Step(`^Malicious attacker stealing auth code from User and using "([^"]*)" ClientID makes /token request and receives "([^"]*)" error$`, s.runOIDC4CIAuthWithErrorInvalidClient) + sc.Step(`^Malicious attacker changed JWT kid header and makes /credential request and receives "([^"]*)" error$`, s.runOIDC4CIAuthWithErrorInvalidSigningKeyID) + sc.Step(`^Malicious attacker changed signature value and makes /credential request and receives "([^"]*)" error$`, s.runOIDC4CIAuthWithErrorInvalidSignatureValue) + sc.Step(`^Malicious attacker changed nonce value and makes /credential request and receives "([^"]*)" error$`, s.runOIDC4CIAuthWithErrorInvalidNonce) // Stress test. sc.Step(`^number of users "([^"]*)" making "([^"]*)" concurrent requests$`, s.getUsersNum) diff --git a/test/stress/pkg/stress/stress_test_case.go b/test/stress/pkg/stress/stress_test_case.go index 4331c9528..569f295ad 100644 --- a/test/stress/pkg/stress/stress_test_case.go +++ b/test/stress/pkg/stress/stress_test_case.go @@ -215,7 +215,7 @@ func (c *TestCase) Invoke() (string, interface{}, error) { CredentialType: c.credentialType, CredentialFormat: c.credentialFormat, Pin: pin, - }) + }, nil) credID := "" if credentials != nil {