Skip to content

Commit

Permalink
feat: use credential offer response for credential request
Browse files Browse the repository at this point in the history
Signed-off-by: Mykhailo Sizov <[email protected]>
  • Loading branch information
mishasizov-SK committed Mar 21, 2024
1 parent 2647524 commit cd82e65
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 61 deletions.
91 changes: 53 additions & 38 deletions component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ func (f *Flow) Run(ctx context.Context) ([]*verifiable.Credential, error) {

f.perfInfo.GetAccessToken = time.Since(start)

return f.receiveVC(token, openIDConfig, credentialIssuer)
return f.receiveVC(token, openIDConfig, credentialOfferResponse, credentialIssuer)
}

func (f *Flow) Signer() jose.Signer {
Expand Down Expand Up @@ -710,6 +710,7 @@ func (f *Flow) getAttestationVP() (string, error) {
func (f *Flow) receiveVC(
token *oauth2.Token,
wellKnown *issuerv1.WellKnownOpenIDIssuerConfiguration,
credentialOfferResponse *oidc4ci.CredentialOfferResponse,
credentialIssuer string,
) ([]*verifiable.Credential, error) {
start := time.Now()
Expand All @@ -722,14 +723,16 @@ func (f *Flow) receiveVC(
return nil, fmt.Errorf("build proof: %w", err)
}

credentialFilters, err := f.getCredentialRequestOIDCCredentialFilters(wellKnown)
credentialFilters, err := f.getCredentialRequestOIDCCredentialFilters(credentialOfferResponse, wellKnown)
if err != nil {
return nil, fmt.Errorf("getCredentialRequestOIDCCredentialFormat: %w", err)
}

var parseCredentialResponseDataList []*parseCredentialResponseData

if f.useBatchCredentialsEndpoint {
canUseBatchCredentialsEndpoint := lo.FromPtr(wellKnown.BatchCredentialEndpoint) != "" && len(credentialFilters) > 0

if canUseBatchCredentialsEndpoint || f.useBatchCredentialsEndpoint {
batchCredentialEndpoint := lo.FromPtr(wellKnown.BatchCredentialEndpoint)
if batchCredentialEndpoint == "" {
return nil, errors.New("BatchCredentialEndpoint is not enalbed for given profile")
Expand Down Expand Up @@ -1003,6 +1006,7 @@ func (f *Flow) doCredentialRequest(
// getCredentialRequestOIDCCredentialFilters returns list of credentialType and credentialFormat pairs
// used for Credential request or Batch Credential request.
func (f *Flow) getCredentialRequestOIDCCredentialFilters(
credentialOfferResponse *oidc4ci.CredentialOfferResponse,
wellKnown *issuerv1.WellKnownOpenIDIssuerConfiguration,
) ([]*credentialFilter, error) {
// Take default value as f.credentialFilters
Expand All @@ -1012,45 +1016,14 @@ func (f *Flow) getCredentialRequestOIDCCredentialFilters(

// CredentialFilters is not supplied:

var credentialFilters []*credentialFilter

if len(f.credentialConfigurationIDs) > 0 {
// CredentialConfigurationID option available so take format from well-known configuration.
for _, credentialConfigurationID := range f.credentialConfigurationIDs {
credentialConf := wellKnown.CredentialConfigurationsSupported.AdditionalProperties[credentialConfigurationID]
format := credentialConf.Format
if format == "" {
return nil, fmt.Errorf(
"unable to obtain OIDC credential format from issuer well-known configuration. "+
"Check if `issuer.credentialMetadata.credential_configurations_supported` contains key `%s` "+
"with nested `format` field", credentialConfigurationID)
}

if credentialConf.CredentialDefinition == nil {
return nil, fmt.Errorf(
"unable to obtain credential type from issuer well-known configuration. "+
"Check if `issuer.credentialMetadata.credential_configurations_supported` contains key `%s` "+
"with nested `credential_definition` field", credentialConfigurationID)
}

credentialType, ok := lo.Find(credentialConf.CredentialDefinition.Type, func(item string) bool {
return item != "VerifiableCredential"
})
if !ok {
return nil, fmt.Errorf(
"unable to get credential type using credential configuration ID %s", credentialConfigurationID)
}

credentialFilters = append(credentialFilters, &credentialFilter{
credentialType: credentialType,
oidcCredentialFormat: vcsverifiable.OIDCFormat(format),
})
}

return credentialFilters, nil
return f.getCredentialFiltersFromCredentialConfigurationIDs(f.credentialConfigurationIDs, wellKnown)
}

if len(f.scopes) > 0 {
var credentialFilters []*credentialFilter

// scopes option available so take format from well-known configuration.
// Spec: https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.2
for _, scope := range f.scopes {
Expand Down Expand Up @@ -1086,7 +1059,49 @@ func (f *Flow) getCredentialRequestOIDCCredentialFilters(
return credentialFilters, nil
}

return nil, errors.New("obtain OIDC credential format")
// Get credential filter from credential offer.
return f.getCredentialFiltersFromCredentialConfigurationIDs(
credentialOfferResponse.CredentialConfigurationIDs, wellKnown)
}

func (f *Flow) getCredentialFiltersFromCredentialConfigurationIDs(
credentialConfigurationIDs []string,
wellKnown *issuerv1.WellKnownOpenIDIssuerConfiguration,
) ([]*credentialFilter, error) {
var credentialFilters []*credentialFilter

for _, credentialConfigurationID := range credentialConfigurationIDs {
credentialConf := wellKnown.CredentialConfigurationsSupported.AdditionalProperties[credentialConfigurationID]
format := credentialConf.Format
if format == "" {
return nil, fmt.Errorf(
"unable to obtain OIDC credential format from issuer well-known configuration. "+
"Check if `issuer.credentialMetadata.credential_configurations_supported` contains key `%s` "+
"with nested `format` field", credentialConfigurationID)
}

if credentialConf.CredentialDefinition == nil {
return nil, fmt.Errorf(
"unable to obtain credential type from issuer well-known configuration. "+
"Check if `issuer.credentialMetadata.credential_configurations_supported` contains key `%s` "+
"with nested `credential_definition` field", credentialConfigurationID)
}

credentialType, ok := lo.Find(credentialConf.CredentialDefinition.Type, func(item string) bool {
return item != "VerifiableCredential"
})
if !ok {
return nil, fmt.Errorf(
"unable to get credential type using credential configuration ID %s", credentialConfigurationID)
}

credentialFilters = append(credentialFilters, &credentialFilter{
credentialType: credentialType,
oidcCredentialFormat: vcsverifiable.OIDCFormat(format),
})
}

return credentialFilters, nil
}

func (f *Flow) handleIssuanceAck(
Expand Down
2 changes: 1 addition & 1 deletion pkg/restapi/v1/oidc4ci/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,7 @@ func (c *Controller) OidcBatchCredential(e echo.Context) error { //nolint:funlen
credentialRequest := cr
did, aud, err = c.HandleProof(ar.GetClient().GetID(), &credentialRequest, session)
if err != nil {
return fmt.Errorf("handle proof: %w", err)
return err
}

var credentialTypes []string
Expand Down
2 changes: 1 addition & 1 deletion pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ func (s *Service) prepareCredentialOffer(

resp := &CredentialOfferResponse{
CredentialIssuer: issuerURL,
CredentialConfigurationIDs: lo.Uniq(credentialConfigurationIDs),
CredentialConfigurationIDs: credentialConfigurationIDs,
Grants: CredentialOfferGrant{},
}

Expand Down
39 changes: 34 additions & 5 deletions pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ func TestService_InitiateIssuance(t *testing.T) {
assert.Equal(t, "did:123", data.DID)
assert.False(t, data.WalletInitiatedIssuance)

assert.Len(t, data.CredentialConfiguration, 2)
assert.Len(t, data.CredentialConfiguration, 3)

prcCredConf := data.CredentialConfiguration[0]
assert.NotEmpty(t, prcCredConf.CredentialTemplate)
Expand All @@ -302,6 +302,18 @@ func TestService_InitiateIssuance(t *testing.T) {
assert.NotEmpty(t, prcCredConf.PreAuthCodeExpiresAt)
assert.Empty(t, prcCredConf.AuthorizationDetails)

univDegreeCredConf = data.CredentialConfiguration[2]
assert.NotEmpty(t, univDegreeCredConf.CredentialTemplate)
assert.Equal(t, verifiable.OIDCFormat("jwt_vc_json"), univDegreeCredConf.OIDCCredentialFormat)
assert.Empty(t, univDegreeCredConf.ClaimEndpoint)
assert.NotEmpty(t, univDegreeCredConf.ClaimDataID)
assert.Equal(t, "vc_name2", univDegreeCredConf.CredentialName)
assert.Equal(t, "vc_desc2", univDegreeCredConf.CredentialDescription)
assert.Equal(t, "UniversityDegreeCredentialIdentifier", univDegreeCredConf.CredentialConfigurationID)
assert.NotEmpty(t, univDegreeCredConf.CredentialExpiresAt)
assert.NotEmpty(t, prcCredConf.PreAuthCodeExpiresAt)
assert.Empty(t, prcCredConf.AuthorizationDetails)

return &oidc4ci.Transaction{
ID: "txID",
TransactionData: oidc4ci.TransactionData{
Expand All @@ -320,23 +332,30 @@ func TestService_InitiateIssuance(t *testing.T) {
},
CredentialConfigurationID: "UniversityDegreeCredentialIdentifier",
},
{
OIDCCredentialFormat: verifiable.JwtVCJsonLD,
CredentialTemplate: &profileapi.CredentialTemplate{
ID: "templateID2",
},
CredentialConfigurationID: "UniversityDegreeCredentialIdentifier",
},
},
},
}, nil
})

mocks.jsonSchemaValidator.EXPECT().Validate(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(nil)
mocks.jsonSchemaValidator.EXPECT().Validate(gomock.Any(), gomock.Any(), gomock.Any()).Times(3).Return(nil)
mocks.pinGenerator.EXPECT().Generate(gomock.Any()).Return("123456789")

chunks := &dataprotect.EncryptedData{
Encrypted: []byte{0x1, 0x2, 0x3},
EncryptedNonce: []byte{0x0, 0x2},
}

mocks.crypto.EXPECT().Encrypt(gomock.Any(), gomock.Any()).Times(2).
mocks.crypto.EXPECT().Encrypt(gomock.Any(), gomock.Any()).Times(3).
Return(chunks, nil)

mocks.claimDataStore.EXPECT().Create(gomock.Any(), gomock.Any()).Times(2).DoAndReturn(
mocks.claimDataStore.EXPECT().Create(gomock.Any(), gomock.Any()).Times(3).DoAndReturn(
func(ctx context.Context, data *oidc4ci.ClaimData) (string, error) {
assert.Equal(t, chunks, data.EncryptedData)

Expand Down Expand Up @@ -413,6 +432,16 @@ func TestService_InitiateIssuance(t *testing.T) {
CredentialName: "vc_name2",
CredentialDescription: "vc_desc2",
},
{
ClaimData: map[string]interface{}{
"key3": "value3",
},
ClaimEndpoint: "",
CredentialTemplateID: "templateID2",
CredentialExpiresAt: now,
CredentialName: "vc_name2",
CredentialDescription: "vc_desc2",
},
},
}

Expand All @@ -421,7 +450,7 @@ func TestService_InitiateIssuance(t *testing.T) {
check: func(t *testing.T, resp *oidc4ci.InitiateIssuanceResponse, err error) {
require.NoError(t, err)
assert.NotNil(t, resp.Tx)
require.Equal(t, "https://wallet.example.com/initiate_issuance?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fvcs.pb.example.com%2Foidc%2Fidp%22%2C%22credential_configuration_ids%22%3A%5B%22PermanentResidentCardIdentifier%22%2C%22UniversityDegreeCredentialIdentifier%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et%22%7D%7D%7D", resp.InitiateIssuanceURL) //nolint
require.Equal(t, "https://wallet.example.com/initiate_issuance?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fvcs.pb.example.com%2Foidc%2Fidp%22%2C%22credential_configuration_ids%22%3A%5B%22PermanentResidentCardIdentifier%22%2C%22UniversityDegreeCredentialIdentifier%22%2C%22UniversityDegreeCredentialIdentifier%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et%22%7D%7D%7D", resp.InitiateIssuanceURL) //nolint
require.Equal(t, oidc4ci.ContentTypeApplicationJSON, resp.ContentType)
},
},
Expand Down
13 changes: 9 additions & 4 deletions test/bdd/features/oidc4vc_api.feature
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ Feature: OIDC4VC REST API
Scenario Outline: OIDC Batch credential issuance and verification Pre Auth flow (request all credentials by credential type)
Given Profile "<issuerProfile>" issuer has been authorized with username "profile-user-issuer-1" and password "profile-user-issuer-1-pwd"
And User holds credential "<credentialType>" with templateID "nil"
And User wants to make credentials request based on credential offer "<useCredentialOfferForCredentialRequest>"
And Profile "<verifierProfile>" verifier has been authorized with username "profile-user-verifier-1" and password "profile-user-verifier-1-pwd"

When User interacts with Wallet to initiate batch credential issuance using pre authorization code flow
Expand All @@ -103,12 +104,13 @@ Feature: OIDC4VC REST API
And Verifier with profile "<verifierProfile>" requests deleted interactions claims
# In examples below Initiate Issuence request and Credential request are based on credentialType param.
Examples:
| issuerProfile | credentialType | issuedCredentialsAmount | verifierProfile | presentationDefinitionID | fields |
| issuerProfile | credentialType | useCredentialOfferForCredentialRequest | issuedCredentialsAmount | verifierProfile | presentationDefinitionID | fields |
# SDJWT issuer, JWT verifier, no limit disclosure in PD query.
| bank_issuer/v1.0 | UniversityDegreeCredential,CrudeProductCredential,VerifiedEmployee | 3 | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id |
| bank_issuer/v1.0 | UniversityDegreeCredential,CrudeProductCredential | 2 | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id |
# | bank_issuer/v1.0 | UniversityDegreeCredential,CrudeProductCredential,VerifiedEmployee | false | 3 | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id |
# | bank_issuer/v1.0 | UniversityDegreeCredential,CrudeProductCredential | false | 2 | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id |
# Same VC type
| bank_issuer/v1.0 | UniversityDegreeCredential,UniversityDegreeCredential | 2 | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id |
# | bank_issuer/v1.0 | UniversityDegreeCredential,UniversityDegreeCredential | false | 2 | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id |
| bank_issuer/v1.0 | UniversityDegreeCredential,UniversityDegreeCredential | true | 2 | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id |

@oidc4vc_rest_auth_flow_credential_conf_id
Scenario Outline: OIDC credential issuance and verification Auth flow using credential configuration ID to request specific credential type
Expand Down Expand Up @@ -277,16 +279,19 @@ 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

@oidc4vc_auth_malicious_attacker_changed_signingKeyID
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

@oidc4vc_auth_malicious_attacker_changed_jwt
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

@oidc4vc_auth_malicious_attacker_changed_nonce
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"
Expand Down
29 changes: 21 additions & 8 deletions test/bdd/pkg/v1/oidc4vc/oidc4vci.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,19 +143,19 @@ func (s *Steps) runOIDC4VCIPreAuth(initiateOIDC4CIRequest initiateOIDC4VCIReques

opts = append(opts, options...)

credentialTypes := strings.Split(s.issuedCredentialType, ",")
if !s.useCredentialOfferCredConfigIDForCredentialRequest {
credentialTypes := strings.Split(s.issuedCredentialType, ",")

// Set option filters
for _, credentialType := range credentialTypes {
format := s.getIssuerOIDCCredentialFormat(credentialType)
opts = append(opts, oidc4vci.WithCredentialFilter(credentialType, format))
// Set option filters
for _, credentialType := range credentialTypes {
format := s.getIssuerOIDCCredentialFormat(credentialType)
opts = append(opts, oidc4vci.WithCredentialFilter(credentialType, format))
}
}

opts = s.addProofBuilder(opts)

flow, err := oidc4vci.NewFlow(s.oidc4vciProvider,
opts...,
)
flow, err := oidc4vci.NewFlow(s.oidc4vciProvider, opts...)
if err != nil {
return fmt.Errorf("init pre-auth flow: %w", err)
}
Expand Down Expand Up @@ -412,6 +412,19 @@ func (s *Steps) credentialTypeTemplateID(issuedCredentialType, issuedCredentialT
return nil
}

// useCredentialOfferForCredentialRequest let's Wallet to create credential request payload
// based on credetnial_configuration_ids from credential offer response rather then credential filters (credentialType & credentialFormat).
// In CLI flow, this behavior can be reproduced via omitting `credential-type` and `credential-format` flags.
// Can be used only for pre authorized flow.
// For testing purpose only.
func (s *Steps) useCredentialOfferForCredentialRequest(trueStr string) error {
use, _ := strconv.ParseBool(trueStr)

s.useCredentialOfferCredConfigIDForCredentialRequest = use

return nil
}

func (s *Steps) runOIDC4CIAuthWithErrorInvalidClient(updatedClientID, errorContains string) error {
resp, err := s.initiateCredentialIssuance(s.getInitiateIssuanceRequestAuthFlow())
if err != nil {
Expand Down
11 changes: 7 additions & 4 deletions test/bdd/pkg/v1/oidc4vc/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ type Steps struct {
wallet *wallet.Wallet
wellKnownService *wellknown.Service

issuedCredentialType string
issuedCredentialTemplateID string
vpClaimsTransactionID string
presentationDefinitionID string
issuedCredentialType string
issuedCredentialTemplateID string
vpClaimsTransactionID string
presentationDefinitionID string
useCredentialOfferCredConfigIDForCredentialRequest bool

// Stress testing
usersNum int
Expand Down Expand Up @@ -89,6 +90,7 @@ func (s *Steps) RegisterSteps(sc *godog.ScenarioContext) {
sc.Step(`^Profile "([^"]*)" issuer has been authorized with username "([^"]*)" and password "([^"]*)"$`, s.authorizeIssuerProfileUser)
sc.Step(`^Profile "([^"]*)" verifier has been authorized with username "([^"]*)" and password "([^"]*)"$`, s.authorizeVerifierProfileUser)
sc.Step(`^User holds credential "([^"]*)" with templateID "([^"]*)"$`, s.credentialTypeTemplateID)
sc.Step(`^User wants to make credentials request based on credential offer "([^"]*)"$`, s.useCredentialOfferForCredentialRequest)
sc.Step(`^User saves issued credentials`, s.saveCredentials)
sc.Step(`^"([^"]*)" credentials are issued$`, s.checkIssuedCredential)
sc.Step(`^issued credential history is updated`, s.checkIssuedCredentialHistoryStep)
Expand Down Expand Up @@ -157,6 +159,7 @@ func (s *Steps) ResetAndSetup() error {
s.stressResult = nil
s.proofType = "jwt"
s.initiateIssuanceApiVersion = ""
s.useCredentialOfferCredConfigIDForCredentialRequest = false
s.composeFeatureEnabled = false
s.composeCredential = nil

Expand Down

0 comments on commit cd82e65

Please sign in to comment.