From c9e6ee570eeafa8fc1a558de5fe51930ef5aa398 Mon Sep 17 00:00:00 2001 From: Andrii Holovko Date: Wed, 6 Nov 2024 11:03:26 +0200 Subject: [PATCH] feat(sdk): credential display API v2 Signed-off-by: Andrii Holovko --- cmd/wallet-sdk-gomobile/display/opts.go | 1 + cmd/wallet-sdk-gomobile/display/resolve.go | 35 +++- .../display/resolve_test.go | 77 +++++-- .../openid4ci/interaction.go | 10 + .../openid4ci/issuerinitiatedinteraction.go | 44 +++- .../verifiable/credential.go | 43 ++++ pkg/credentialschema/credentialdisplay.go | 80 +++---- pkg/credentialschema/credentialschema.go | 18 +- pkg/credentialschema/credentialschema_test.go | 6 +- pkg/credentialschema/opts.go | 94 +++++++-- pkg/openid4ci/issuerinitiatedinteraction.go | 7 + .../university_degree_v2.json | 21 +- .../fixtures/profile/profiles.json | 56 +++++ test/integration/openid4ci_test.go | 197 +++++++++++++----- test/integration/pkg/helpers/displaydata.go | 53 +++++ 15 files changed, 578 insertions(+), 164 deletions(-) diff --git a/cmd/wallet-sdk-gomobile/display/opts.go b/cmd/wallet-sdk-gomobile/display/opts.go index c0f1d816..41dff88f 100644 --- a/cmd/wallet-sdk-gomobile/display/opts.go +++ b/cmd/wallet-sdk-gomobile/display/opts.go @@ -22,6 +22,7 @@ type Opts struct { maskingString *string didResolver api.DIDResolver skipNonClaimData bool + credentialConfigIDs []string } // NewOpts returns a new Opts object. diff --git a/cmd/wallet-sdk-gomobile/display/resolve.go b/cmd/wallet-sdk-gomobile/display/resolve.go index 9142b1d4..daea8a97 100644 --- a/cmd/wallet-sdk-gomobile/display/resolve.go +++ b/cmd/wallet-sdk-gomobile/display/resolve.go @@ -9,17 +9,14 @@ package display import ( "errors" - "github.com/trustbloc/vc-go/proof/defaults" + afgoverifiable "github.com/trustbloc/vc-go/verifiable" "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/api" "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/openid4ci" - "github.com/trustbloc/wallet-sdk/pkg/common" - - afgoverifiable "github.com/trustbloc/vc-go/verifiable" - "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/verifiable" "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/wrapper" + "github.com/trustbloc/wallet-sdk/pkg/common" goapicredentialschema "github.com/trustbloc/wallet-sdk/pkg/credentialschema" ) @@ -44,8 +41,30 @@ func Resolve(vcs *verifiable.CredentialsArray, issuerURI string, opts *Opts) (*D return &Data{resolvedDisplayData: resolvedDisplayData}, nil } -func ResolveCredential(vcs *verifiable.CredentialsArray, issuerURI string, opts *Opts) (*Resolved, error) { - goAPIOpts, err := generateGoAPIOpts(vcs, issuerURI, opts) +func ResolveCredential(credentialsArray *verifiable.CredentialsArray, issuerURI string, opts *Opts) (*Resolved, error) { + goAPIOpts, err := generateGoAPIOpts(credentialsArray, issuerURI, opts) + if err != nil { + return nil, err + } + + resolvedDisplayData, err := goapicredentialschema.ResolveCredential(goAPIOpts...) + if err != nil { + return nil, err + } + + return &Resolved{resolvedDisplayData: resolvedDisplayData}, nil +} + +func ResolveCredentialV2(credentialsArray *verifiable.CredentialsArrayV2, issuerURI string, opts *Opts) (*Resolved, error) { + credentials := &verifiable.CredentialsArray{} + opts.credentialConfigIDs = make([]string, credentialsArray.Length()) + + for i := 0; i < credentialsArray.Length(); i++ { + credentials.Add(credentialsArray.AtIndex(i)) + opts.credentialConfigIDs[i] = credentialsArray.ConfigIDAtIndex(i) + } + + goAPIOpts, err := generateGoAPIOpts(credentials, issuerURI, opts) if err != nil { return nil, err } @@ -89,7 +108,7 @@ func generateGoAPIOpts(vcs *verifiable.CredentialsArray, issuerURI string, httpClient := wrapper.NewHTTPClient(opts.httpTimeout, opts.additionalHeaders, opts.disableHTTPClientTLSVerification) goAPIOpts := []goapicredentialschema.ResolveOpt{ - goapicredentialschema.WithCredentials(mobileVCsArrayToGoAPIVCsArray(vcs)), + goapicredentialschema.WithCredentials(mobileVCsArrayToGoAPIVCsArray(vcs), opts.credentialConfigIDs...), goapicredentialschema.WithIssuerURI(issuerURI), goapicredentialschema.WithPreferredLocale(opts.preferredLocale), goapicredentialschema.WithHTTPClient(httpClient), diff --git a/cmd/wallet-sdk-gomobile/display/resolve_test.go b/cmd/wallet-sdk-gomobile/display/resolve_test.go index 434ff6d6..a8ffe955 100644 --- a/cmd/wallet-sdk-gomobile/display/resolve_test.go +++ b/cmd/wallet-sdk-gomobile/display/resolve_test.go @@ -15,15 +15,14 @@ import ( "strconv" "testing" - "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/did" - "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/openid4ci" - "github.com/trustbloc/wallet-sdk/pkg/models/issuer" - "github.com/stretchr/testify/require" "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/api" + "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/did" "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/display" + "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/openid4ci" "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/verifiable" + "github.com/trustbloc/wallet-sdk/pkg/models/issuer" ) const ( @@ -215,33 +214,65 @@ func TestResolveCredential(t *testing.T) { parseVCOptionalArgs := verifiable.NewOpts() parseVCOptionalArgs.DisableProofCheck() - vc, err := verifiable.ParseCredential(credentialUniversityDegree, parseVCOptionalArgs) - require.NoError(t, err) + t.Run("Success: CredentialArray v1", func(t *testing.T) { + vc, err := verifiable.ParseCredential(credentialUniversityDegree, parseVCOptionalArgs) + require.NoError(t, err) - vcs := verifiable.NewCredentialsArray() - vcs.Add(vc) + vcs := verifiable.NewCredentialsArray() + vcs.Add(vc) - opts := display.NewOpts().SetMaskingString("*") + opts := display.NewOpts().SetMaskingString("*") - resolvedDisplayData, err := display.ResolveCredential(vcs, server.URL, opts) - require.NoError(t, err) + resolvedDisplayData, err := display.ResolveCredential(vcs, server.URL, opts) + require.NoError(t, err) - require.Equal(t, resolvedDisplayData.LocalizedIssuersLength(), 2) - require.Equal(t, resolvedDisplayData.CredentialsLength(), 1) - require.Equal(t, resolvedDisplayData.CredentialAtIndex(0).LocalizedOverviewsLength(), 1) - require.Equal(t, resolvedDisplayData.CredentialAtIndex(0).SubjectsLength(), 6) + require.Equal(t, resolvedDisplayData.LocalizedIssuersLength(), 2) + require.Equal(t, resolvedDisplayData.CredentialsLength(), 1) + require.Equal(t, resolvedDisplayData.CredentialAtIndex(0).LocalizedOverviewsLength(), 1) + require.Equal(t, resolvedDisplayData.CredentialAtIndex(0).SubjectsLength(), 6) - credentialDisplay := resolvedDisplayData.CredentialAtIndex(0) + credentialDisplay := resolvedDisplayData.CredentialAtIndex(0) - for i := 0; i < credentialDisplay.SubjectsLength(); i++ { - claim := credentialDisplay.SubjectAtIndex(i) + for i := 0; i < credentialDisplay.SubjectsLength(); i++ { + claim := credentialDisplay.SubjectAtIndex(i) - if claim.LocalizedLabelAtIndex(0).Name() == sensitiveIDLabel { - require.Equal(t, "*****6789", claim.Value()) - } else if claim.LocalizedLabelAtIndex(0).Name() == reallySensitiveIDLabel { - require.Equal(t, "*******", claim.Value()) + if claim.LocalizedLabelAtIndex(0).Name() == sensitiveIDLabel { + require.Equal(t, "*****6789", claim.Value()) + } else if claim.LocalizedLabelAtIndex(0).Name() == reallySensitiveIDLabel { + require.Equal(t, "*******", claim.Value()) + } } - } + }) + + t.Run("Success: CredentialArray v2", func(t *testing.T) { + vc, err := verifiable.ParseCredential(credentialUniversityDegree, parseVCOptionalArgs) + require.NoError(t, err) + + vcs := verifiable.NewCredentialsArrayV2() + vcs.Add(vc, "UniversityDegreeCredential_jwt_vc_json_v1") + + opts := display.NewOpts().SetMaskingString("*") + + resolvedDisplayData, err := display.ResolveCredentialV2(vcs, server.URL, opts) + require.NoError(t, err) + + require.Equal(t, resolvedDisplayData.LocalizedIssuersLength(), 2) + require.Equal(t, resolvedDisplayData.CredentialsLength(), 1) + require.Equal(t, resolvedDisplayData.CredentialAtIndex(0).LocalizedOverviewsLength(), 1) + require.Equal(t, resolvedDisplayData.CredentialAtIndex(0).SubjectsLength(), 6) + + credentialDisplay := resolvedDisplayData.CredentialAtIndex(0) + + for i := 0; i < credentialDisplay.SubjectsLength(); i++ { + claim := credentialDisplay.SubjectAtIndex(i) + + if claim.LocalizedLabelAtIndex(0).Name() == sensitiveIDLabel { + require.Equal(t, "*****6789", claim.Value()) + } else if claim.LocalizedLabelAtIndex(0).Name() == reallySensitiveIDLabel { + require.Equal(t, "*******", claim.Value()) + } + } + }) } func TestResolveCredentialOffer(t *testing.T) { diff --git a/cmd/wallet-sdk-gomobile/openid4ci/interaction.go b/cmd/wallet-sdk-gomobile/openid4ci/interaction.go index ba0dcee6..9fe02288 100644 --- a/cmd/wallet-sdk-gomobile/openid4ci/interaction.go +++ b/cmd/wallet-sdk-gomobile/openid4ci/interaction.go @@ -109,3 +109,13 @@ func toGomobileCredentials(credentials []*afgoverifiable.Credential) *verifiable return gomobileCredentials } + +func toGomobileCredentialsV2(credentials []*afgoverifiable.Credential, configIDs []string) *verifiable.CredentialsArrayV2 { + credentialArray := verifiable.NewCredentialsArrayV2() + + for i := range credentials { + credentialArray.Add(verifiable.NewCredential(credentials[i]), configIDs[i]) + } + + return credentialArray +} diff --git a/cmd/wallet-sdk-gomobile/openid4ci/issuerinitiatedinteraction.go b/cmd/wallet-sdk-gomobile/openid4ci/issuerinitiatedinteraction.go index 1ef42e6c..e941794f 100644 --- a/cmd/wallet-sdk-gomobile/openid4ci/issuerinitiatedinteraction.go +++ b/cmd/wallet-sdk-gomobile/openid4ci/issuerinitiatedinteraction.go @@ -10,14 +10,16 @@ package openid4ci import ( "errors" + "fmt" - "github.com/trustbloc/wallet-sdk/pkg/walleterror" + verifiableapi "github.com/trustbloc/vc-go/verifiable" "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/api" "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/otel" "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/verifiable" "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/wrapper" openid4cigoapi "github.com/trustbloc/wallet-sdk/pkg/openid4ci" + "github.com/trustbloc/wallet-sdk/pkg/walleterror" ) // IssuerInitiatedInteraction represents a single issuer-instantiated OpenID4CI interaction between a wallet and an @@ -102,13 +104,38 @@ func (i *IssuerInitiatedInteraction) CreateAuthorizationURL(clientID, redirectUR func (i *IssuerInitiatedInteraction) RequestCredentialWithPreAuth( vm *api.VerificationMethod, opts *RequestCredentialWithPreAuthOpts, ) (*verifiable.CredentialsArray, error) { + credentials, _, err := i.requestCredentialWithPreAuth(vm, opts) + if err != nil { + return nil, wrapper.ToMobileErrorWithTrace(err, i.oTel) + } + + return toGomobileCredentials(credentials), nil +} + +// RequestCredentialWithPreAuthV2 requests credentials using a pre-authorized code flow. +// Returns an array of credentials with config IDs, which map to CredentialConfigurationSupported in the +// issuer's metadata. +func (i *IssuerInitiatedInteraction) RequestCredentialWithPreAuthV2( + vm *api.VerificationMethod, opts *RequestCredentialWithPreAuthOpts, +) (*verifiable.CredentialsArrayV2, error) { + credentials, configIDs, err := i.requestCredentialWithPreAuth(vm, opts) + if err != nil { + return nil, wrapper.ToMobileErrorWithTrace(err, i.oTel) + } + + return toGomobileCredentialsV2(credentials, configIDs), nil +} + +func (i *IssuerInitiatedInteraction) requestCredentialWithPreAuth( + vm *api.VerificationMethod, opts *RequestCredentialWithPreAuthOpts, +) ([]*verifiableapi.Credential, []string, error) { if opts == nil { opts = NewRequestCredentialWithPreAuthOpts() } signer, err := createSigner(vm, i.crypto) if err != nil { - return nil, wrapper.ToMobileErrorWithTrace(err, i.oTel) + return nil, nil, wrapper.ToMobileErrorWithTrace(err, i.oTel) } goOpts := []openid4cigoapi.RequestCredentialWithPreAuthOpt{openid4cigoapi.WithPIN(opts.pin)} @@ -116,7 +143,7 @@ func (i *IssuerInitiatedInteraction) RequestCredentialWithPreAuth( if opts.attestationVM != nil { attestationSigner, attErr := createSigner(opts.attestationVM, i.crypto) if attErr != nil { - return nil, wrapper.ToMobileErrorWithTrace(attErr, i.oTel) + return nil, nil, wrapper.ToMobileErrorWithTrace(attErr, i.oTel) } goOpts = append(goOpts, openid4cigoapi.WithAttestationVC(attestationSigner, opts.attestationVC)) @@ -124,10 +151,17 @@ func (i *IssuerInitiatedInteraction) RequestCredentialWithPreAuth( credentials, err := i.goAPIInteraction.RequestCredentialWithPreAuth(signer, goOpts...) if err != nil { - return nil, wrapper.ToMobileErrorWithTrace(err, i.oTel) + return nil, nil, wrapper.ToMobileErrorWithTrace(err, i.oTel) } - return toGomobileCredentials(credentials), nil + configIDs := i.goAPIInteraction.CredentialConfigIDs() + + if len(credentials) != len(configIDs) { + return nil, nil, fmt.Errorf("mismatch in the number of credentials and configuration IDs: "+ + "expected %d but got %d", len(credentials), len(configIDs)) + } + + return credentials, configIDs, nil } // RequestCredentialWithAuth requests credential(s) from the issuer. This method can only be used for the diff --git a/cmd/wallet-sdk-gomobile/verifiable/credential.go b/cmd/wallet-sdk-gomobile/verifiable/credential.go index b1e64fd3..c35ad78c 100644 --- a/cmd/wallet-sdk-gomobile/verifiable/credential.go +++ b/cmd/wallet-sdk-gomobile/verifiable/credential.go @@ -204,3 +204,46 @@ func (a *CredentialsArray) AtIndex(index int) *Credential { return a.credentials[index] } + +// CredentialsArrayV2 represents an array of Credentials with associated array of config IDs. +// Each config ID maps to CredentialConfigurationSupported in the issuer's metadata. +type CredentialsArrayV2 struct { + credentials []*Credential + configIDs []string +} + +// NewCredentialsArrayV2 creates a new CredentialsArrayV2. +func NewCredentialsArrayV2() *CredentialsArrayV2 { + return &CredentialsArrayV2{} +} + +// Add adds a new Credential with associated credential config ID. +func (a *CredentialsArrayV2) Add(credential *Credential, configID string) { + a.credentials = append(a.credentials, credential) + a.configIDs = append(a.configIDs, configID) +} + +// Length returns the number of Credentials contained within this CredentialsArrayV2. +func (a *CredentialsArrayV2) Length() int { + return len(a.credentials) +} + +// AtIndex returns the Credential at the given index. +// If the index passed in is out of bounds, then nil is returned. +func (a *CredentialsArrayV2) AtIndex(index int) *Credential { + if index < 0 || index >= len(a.credentials) { + return nil + } + + return a.credentials[index] +} + +// ConfigIDAtIndex returns the config ID for the Credential at the given index. +// If the index is out of bounds, it returns an empty string. +func (a *CredentialsArrayV2) ConfigIDAtIndex(index int) string { + if index < 0 || index >= len(a.credentials) { + return "" + } + + return a.configIDs[index] +} diff --git a/pkg/credentialschema/credentialdisplay.go b/pkg/credentialschema/credentialdisplay.go index 997e93a1..26ef6d09 100644 --- a/pkg/credentialschema/credentialdisplay.go +++ b/pkg/credentialschema/credentialdisplay.go @@ -27,13 +27,18 @@ var ( const defaultLocale = "en-US" func buildCredentialDisplays( - vcs []*verifiable.Credential, - credentialConfigurationsSupported map[issuer.CredentialConfigurationID]*issuer.CredentialConfigurationSupported, + credentialConfigMappings []*credentialConfigMapping, preferredLocale, maskingString string, ) ([]CredentialDisplay, error) { var credentialDisplays []CredentialDisplay - for _, vc := range vcs { + for _, m := range credentialConfigMappings { + vc := m.credential + + if vc == nil { + return nil, errors.New("no credential specified") + } + // The call below creates a copy of the VC with the selective disclosures merged into the credential subject. displayVC, err := vc.CreateDisplayCredential(verifiable.DisplayAllDisclosures()) if err != nil { @@ -45,34 +50,27 @@ func buildCredentialDisplays( return nil, err } - var foundMatchingType bool + var credentialDisplay *CredentialDisplay - for _, credentialConfigurationSupported := range credentialConfigurationsSupported { - if !haveMatchingTypes(credentialConfigurationSupported, displayVC.Contents().Types) { - continue + if len(m.config) > 0 { + var config *issuer.CredentialConfigurationSupported + for _, c := range m.config { + config = c + break } - credentialDisplay, err := buildCredentialDisplay(credentialConfigurationSupported, vc, subject, preferredLocale, - maskingString) + credentialDisplay, err = buildCredentialDisplay(config, vc, subject, preferredLocale, maskingString) if err != nil { return nil, err } - - credentialDisplays = append(credentialDisplays, *credentialDisplay) - - foundMatchingType = true - - break - } - - if !foundMatchingType { + } else { // In case the issuer's metadata doesn't contain display info for this type of credential for some // reason, we build up a default/generic type of credential display based only on information in the VC. // It'll be functional, but won't be pretty. - credentialDisplay := buildDefaultCredentialDisplay(vc.Contents().ID, subject) - - credentialDisplays = append(credentialDisplays, *credentialDisplay) + credentialDisplay = buildDefaultCredentialDisplay(vc.Contents().ID, subject) } + + credentialDisplays = append(credentialDisplays, *credentialDisplay) } return credentialDisplays, nil @@ -413,14 +411,29 @@ func convertLogo(logo *issuer.Logo) *Logo { } func buildCredentialDisplaysAllLocale( - vcs []*verifiable.Credential, - credentialConfigurationsSupported map[issuer.CredentialConfigurationID]*issuer.CredentialConfigurationSupported, + credentialConfigMappings []*credentialConfigMapping, maskingString string, skipNonClaimData bool, ) ([]Credential, error) { var credentialDisplays []Credential - for _, vc := range vcs { + for _, m := range credentialConfigMappings { + vc := m.credential + + if vc == nil { + return nil, errors.New("no credential specified") + } + + var config *issuer.CredentialConfigurationSupported + for _, c := range m.config { + config = c + break + } + + if config == nil { + return nil, errors.New("no credential configuration specified") + } + // The call below creates a copy of the VC with the selective disclosures merged into the credential subject. displayVC, err := vc.CreateDisplayCredential(verifiable.DisplayAllDisclosures()) if err != nil { @@ -432,21 +445,12 @@ func buildCredentialDisplaysAllLocale( return nil, err } - for _, credentialConfigurationSupported := range credentialConfigurationsSupported { - if !haveMatchingTypes(credentialConfigurationSupported, displayVC.Contents().Types) { - continue - } - - credentialDisplay, err := buildCredentialDisplayAllLocale(credentialConfigurationSupported, vc, subject, - maskingString, skipNonClaimData) - if err != nil { - return nil, err - } - - credentialDisplays = append(credentialDisplays, *credentialDisplay) - - break + credentialDisplay, err := buildCredentialDisplayAllLocale(config, vc, subject, maskingString, skipNonClaimData) + if err != nil { + return nil, err } + + credentialDisplays = append(credentialDisplays, *credentialDisplay) } return credentialDisplays, nil diff --git a/pkg/credentialschema/credentialschema.go b/pkg/credentialschema/credentialschema.go index ac361b81..ae9c3d96 100644 --- a/pkg/credentialschema/credentialschema.go +++ b/pkg/credentialschema/credentialschema.go @@ -7,14 +7,16 @@ SPDX-License-Identifier: Apache-2.0 // Package credentialschema contains a function that can be used to resolve display values per the OpenID4CI spec. package credentialschema -import "github.com/trustbloc/wallet-sdk/pkg/models/issuer" +import ( + "github.com/trustbloc/wallet-sdk/pkg/models/issuer" +) // Resolve resolves display information for some issued credentials based on an issuer's metadata. // The CredentialDisplays in the returned ResolvedDisplayData object correspond to the VCs passed in and are in the // same order. // This method requires one VC source and one issuer metadata source. See opts.go for more information. func Resolve(opts ...ResolveOpt) (*ResolvedDisplayData, error) { - vcs, metadata, preferredLocale, maskingString, err := processOpts(opts) + credentialConfigMappings, issuerMetadata, preferredLocale, maskingString, err := processOpts(opts) if err != nil { return nil, err } @@ -24,13 +26,13 @@ func Resolve(opts ...ResolveOpt) (*ResolvedDisplayData, error) { maskingString = &defaultMaskingString } - credentialDisplays, err := buildCredentialDisplays(vcs, metadata.CredentialConfigurationsSupported, preferredLocale, - *maskingString) + credentialDisplays, err := buildCredentialDisplays(credentialConfigMappings, + preferredLocale, *maskingString) if err != nil { return nil, err } - issuerOverview := getIssuerDisplay(metadata.LocalizedIssuerDisplays, preferredLocale) + issuerOverview := getIssuerDisplay(issuerMetadata.LocalizedIssuerDisplays, preferredLocale) return &ResolvedDisplayData{ IssuerDisplay: issuerOverview, @@ -40,7 +42,7 @@ func Resolve(opts ...ResolveOpt) (*ResolvedDisplayData, error) { // ResolveCredential resolves display information for some issued credentials based on an issuer's metadata. func ResolveCredential(opts ...ResolveOpt) (*ResolvedData, error) { - vcs, metadata, _, maskingString, err := processOpts(opts) + credentialConfigMappings, issuerMetadata, _, maskingString, err := processOpts(opts) if err != nil { return nil, err } @@ -52,13 +54,13 @@ func ResolveCredential(opts ...ResolveOpt) (*ResolvedData, error) { maskingString = &defaultMaskingString } - credentialDisplays, err := buildCredentialDisplaysAllLocale(vcs, metadata.CredentialConfigurationsSupported, + credentialDisplays, err := buildCredentialDisplaysAllLocale(credentialConfigMappings, *maskingString, rOpts.skipNonClaimData) if err != nil { return nil, err } - issuerOverview := getIssuerDisplayAllLocale(metadata.LocalizedIssuerDisplays) + issuerOverview := getIssuerDisplayAllLocale(issuerMetadata.LocalizedIssuerDisplays) return &ResolvedData{ LocalizedIssuer: issuerOverview, diff --git a/pkg/credentialschema/credentialschema_test.go b/pkg/credentialschema/credentialschema_test.go index eb7320db..3807d9ee 100644 --- a/pkg/credentialschema/credentialschema_test.go +++ b/pkg/credentialschema/credentialschema_test.go @@ -13,14 +13,12 @@ import ( "net/http/httptest" "testing" - "github.com/trustbloc/kms-go/doc/jose" - - "github.com/trustbloc/wallet-sdk/pkg/memstorage" - "github.com/stretchr/testify/require" + "github.com/trustbloc/kms-go/doc/jose" "github.com/trustbloc/vc-go/verifiable" "github.com/trustbloc/wallet-sdk/pkg/credentialschema" + "github.com/trustbloc/wallet-sdk/pkg/memstorage" "github.com/trustbloc/wallet-sdk/pkg/models/issuer" ) diff --git a/pkg/credentialschema/opts.go b/pkg/credentialschema/opts.go index 6cdd81e6..047b5531 100644 --- a/pkg/credentialschema/opts.go +++ b/pkg/credentialschema/opts.go @@ -8,10 +8,10 @@ package credentialschema import ( "errors" + "fmt" "net/http" "github.com/trustbloc/vc-go/jwt" - "github.com/trustbloc/vc-go/verifiable" "github.com/trustbloc/wallet-sdk/pkg/api" @@ -31,6 +31,9 @@ type credentialSource struct { reader credentialReader // ids specifies which credentials should be retrieved from the reader. ids []string + // credentialConfigIDs holds the config IDs for credentials, with each ID corresponding to the credential + // at the same index in the vcs slice. + credentialConfigIDs []string } // issuerMetadataSource represents the different ways that issuer metadata can be specified in the Resolve function. @@ -43,6 +46,12 @@ type issuerMetadataSource struct { metadata *issuer.Metadata } +// credentialConfigMapping represents a mapping of Credential to its corresponding CredentialConfigurationSupported +type credentialConfigMapping struct { + credential *verifiable.Credential + config map[string]*issuer.CredentialConfigurationSupported // config ID -> CredentialConfigurationSupported +} + type httpClient interface { Do(req *http.Request) (*http.Response, error) } @@ -62,9 +71,10 @@ type resolveOpts struct { type ResolveOpt func(opts *resolveOpts) // WithCredentials is an option allowing a caller to directly pass in the VCs that they want to have resolved. -func WithCredentials(vcs []*verifiable.Credential) ResolveOpt { +func WithCredentials(vcs []*verifiable.Credential, configID ...string) ResolveOpt { return func(opts *resolveOpts) { opts.credentialSource.vcs = vcs + opts.credentialSource.credentialConfigIDs = configID } } @@ -170,7 +180,7 @@ func WithSkipNonClaimData() ResolveOpt { } } -func processOpts(opts []ResolveOpt) ([]*verifiable.Credential, *issuer.Metadata, string, *string, error) { +func processOpts(opts []ResolveOpt) ([]*credentialConfigMapping, *issuer.Metadata, string, *string, error) { mergedOpts := mergeOpts(opts) err := validateOpts(mergedOpts) @@ -204,9 +214,19 @@ func validateVCOpts(credentialSource *credentialSource) error { return errors.New("no credentials specified") } - if credentialSource.vcs != nil && credentialSource.reader != nil { - return errors.New("cannot have multiple credential sources specified - must use either " + - "WithCredentials or WithCredentialReader, but not both") + if credentialSource.vcs != nil { + if credentialSource.reader != nil { + return errors.New("cannot have multiple credential sources specified - must use either " + + "WithCredentials or WithCredentialReader, but not both") + } + + numConfigIDs := len(credentialSource.credentialConfigIDs) + numVCs := len(credentialSource.vcs) + + if numConfigIDs > 0 && numConfigIDs != numVCs { + return fmt.Errorf("mismatch between the number of credentials (%d) and the number of config IDs (%d)", + numVCs, numConfigIDs) + } } if credentialSource.reader != nil && len(credentialSource.ids) == 0 { @@ -224,8 +244,8 @@ func validateIssuerMetadataOpts(issuerMetadataSource *issuerMetadataSource) erro return nil } -func processValidatedOpts(opts *resolveOpts) ([]*verifiable.Credential, *issuer.Metadata, string, *string, error) { - vcs, err := processVCOpts(&opts.credentialSource) +func processValidatedOpts(opts *resolveOpts) ([]*credentialConfigMapping, *issuer.Metadata, string, *string, error) { + credentialConfigMappings, err := processVCOpts(&opts.credentialSource) if err != nil { return nil, nil, "", nil, err } @@ -248,14 +268,55 @@ func processValidatedOpts(opts *resolveOpts) ([]*verifiable.Credential, *issuer. return nil, nil, "", nil, err } - return vcs, issuerMetadata, opts.preferredLocal, opts.maskingString, nil + for _, m := range credentialConfigMappings { + vc := m.credential + + if len(m.config) > 0 { + for configID := range m.config { + config, ok := issuerMetadata.CredentialConfigurationsSupported[configID] + if !ok { + return nil, nil, "", nil, errors.New(fmt.Sprintf("credential configuration with ID %s not found", + configID)) + } + + m.config[configID] = config + } + + continue + } + + for configID, config := range issuerMetadata.CredentialConfigurationsSupported { + if !haveMatchingTypes(config, vc.Contents().Types) { + continue + } + + m.config[configID] = config + break + } + } + + return credentialConfigMappings, issuerMetadata, opts.preferredLocal, opts.maskingString, nil } -func processVCOpts(credentialSource *credentialSource) ([]*verifiable.Credential, error) { - var vcs []*verifiable.Credential +func processVCOpts(credentialSource *credentialSource) ([]*credentialConfigMapping, error) { + var credentialConfigMappings []*credentialConfigMapping if credentialSource.vcs != nil { - vcs = credentialSource.vcs + numVCs := len(credentialSource.vcs) + numConfigIDs := len(credentialSource.credentialConfigIDs) + + for i := 0; i < numVCs; i++ { + m := &credentialConfigMapping{ + credential: credentialSource.vcs[i], + config: make(map[string]*issuer.CredentialConfigurationSupported), + } + + if numConfigIDs > 0 && i < numConfigIDs { + m.config[credentialSource.credentialConfigIDs[i]] = nil + } + + credentialConfigMappings = append(credentialConfigMappings, m) + } } else { for _, id := range credentialSource.ids { vc, err := credentialSource.reader.Get(id) @@ -263,11 +324,16 @@ func processVCOpts(credentialSource *credentialSource) ([]*verifiable.Credential return nil, err } - vcs = append(vcs, vc) + credentialConfigMappings = append(credentialConfigMappings, + &credentialConfigMapping{ + credential: vc, + config: make(map[string]*issuer.CredentialConfigurationSupported), + }, + ) } } - return vcs, nil + return credentialConfigMappings, nil } func processIssuerMetadataOpts(issuerMetadataSource *issuerMetadataSource, httpClient httpClient, diff --git a/pkg/openid4ci/issuerinitiatedinteraction.go b/pkg/openid4ci/issuerinitiatedinteraction.go index 3bef0cb3..b79f01d4 100644 --- a/pkg/openid4ci/issuerinitiatedinteraction.go +++ b/pkg/openid4ci/issuerinitiatedinteraction.go @@ -64,6 +64,7 @@ type IssuerInitiatedInteraction struct { credentialTypes [][]string credentialFormats []string + credentialConfigIDs []string preAuthorizedCodeGrantParams *PreAuthorizedCodeGrantParams authorizationCodeGrantParams *AuthorizationCodeGrantParams @@ -125,6 +126,7 @@ func NewIssuerInitiatedInteraction( authorizationCodeGrantParams: authorizationCodeGrantParams, credentialTypes: credentialTypes, credentialFormats: credentialFormats, + credentialConfigIDs: credentialOffer.CredentialConfigurationIDs, }, config.MetricsLogger.Log( &api.MetricsEvent{ @@ -233,6 +235,11 @@ func (i *IssuerInitiatedInteraction) PreAuthorizedCodeGrantParams() (*PreAuthori return i.preAuthorizedCodeGrantParams, nil } +// CredentialConfigIDs returns the credential config IDs from the credential offer. +func (i *IssuerInitiatedInteraction) CredentialConfigIDs() []string { + return i.credentialConfigIDs +} + // AuthorizationCodeGrantTypeSupported indicates whether the issuer supports the authorization code grant type. func (i *IssuerInitiatedInteraction) AuthorizationCodeGrantTypeSupported() bool { return i.authorizationCodeGrantParams != nil diff --git a/test/integration/expecteddisplaydata/university_degree_v2.json b/test/integration/expecteddisplaydata/university_degree_v2.json index 8ebb5a95..d9cb7824 100644 --- a/test/integration/expecteddisplaydata/university_degree_v2.json +++ b/test/integration/expecteddisplaydata/university_degree_v2.json @@ -12,6 +12,7 @@ "uri": "https://example.com/public/logo.png", "alt_text": "a square logo of a degree verification" }, + "url": "http://localhost:8075", "background_color": "#12107c", "text_color": "#FFFFFF" }, @@ -34,21 +35,8 @@ "raw_id": "degree", "label": "Degree", "value_type": "string", - "raw_value": "MIT", - "locale": "en-US" - }, - { - "raw_id": "degreeSchool", - "label": "Degree School", - "value_type": "string", - "raw_value": "MIT school", + "raw_value": "MS", "locale": "en-US" - }, - { - "raw_id": "photo", - "label": "Photo", - "value_type": "image", - "raw_value": "binary data" } ] }, @@ -60,6 +48,7 @@ "uri": "https://example.com/public/logo.png", "alt_text": "a square logo of a degree verification" }, + "url": "http://localhost:8075", "background_color": "#12107c", "text_color": "#FFFFFF" }, @@ -82,14 +71,14 @@ "raw_id": "degree", "label": "Degree", "value_type": "string", - "raw_value": "MS", + "raw_value": "MIT", "locale": "en-US" }, { "raw_id": "degreeSchool", "label": "Degree School", "value_type": "string", - "raw_value": "Stanford", + "raw_value": "MIT school", "locale": "en-US" }, { diff --git a/test/integration/fixtures/profile/profiles.json b/test/integration/fixtures/profile/profiles.json index a736be0e..b2bb2c0f 100644 --- a/test/integration/fixtures/profile/profiles.json +++ b/test/integration/fixtures/profile/profiles.json @@ -1139,6 +1139,62 @@ "text_color": "#FFFFFF" } ] + }, + "UniversityDegreeCredential_ldp_vc_v2": { + "format": "ldp_vc", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "familyName": { + "display": [ + { + "name": "Family Name", + "locale": "en-US" + } + ], + "value_type": "string" + }, + "givenName": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ], + "value_type": "string" + }, + "degree": { + "display": [ + { + "name": "Degree", + "locale": "en-US" + } + ], + "value_type": "string" + } + } + }, + "cryptographic_binding_methods_supported": [ + "did" + ], + "credential_signing_alg_values_supported": [ + "Ed25519Signature2018" + ], + "display": [ + { + "name": "University Degree Credential", + "locale": "en-US", + "logo": { + "uri": "https://example.com/public/logo.png", + "alt_text": "a square logo of a degree verification" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] } } } diff --git a/test/integration/openid4ci_test.go b/test/integration/openid4ci_test.go index e6021d46..21a74625 100644 --- a/test/integration/openid4ci_test.go +++ b/test/integration/openid4ci_test.go @@ -10,8 +10,10 @@ package integration import ( "crypto/tls" _ "embed" + "encoding/json" "fmt" "net/http" + "net/url" "strings" "testing" "time" @@ -77,6 +79,7 @@ type test struct { claims []*claimEntry acknowledgeReject bool trustInfo bool + displayAPIv2 bool } type claimEntry struct { @@ -248,23 +251,21 @@ func doPreAuthCodeFlowTest(t *testing.T) { }, expectedDisplayData: helpers.ParseDisplayData(t, expectedUniversityDegreeV2), expectedIssuerURI: "http://localhost:8075/oidc/idp/university_degree_issuer_v2/v1.0", + displayAPIv2: true, }, } - oidc4ciSetup, err := oidc4ci.NewSetup(testenv.NewHttpRequest()) - require.NoError(t, err) - - err = oidc4ciSetup.AuthorizeIssuerBypassAuth(organizationID, vcsAPIDirectURL) - require.NoError(t, err) + oidc4ciSetup, setupErr := oidc4ci.NewSetup(testenv.NewHttpRequest()) + require.NoError(t, setupErr) - //vcStatusVerifier, err := credential.NewStatusVerifier(credential.NewStatusVerifierOpts()) - //require.NoError(t, err) + setupErr = oidc4ciSetup.AuthorizeIssuerBypassAuth(organizationID, vcsAPIDirectURL) + require.NoError(t, setupErr) var traceIDs []string for _, tc := range preAuthTests { - fmt.Println(fmt.Sprintf("running tests with issuerProfileID=%s issuerDIDMethod=%s walletDIDMethod=%s", - tc.issuerProfileID, tc.issuerDIDMethod, tc.walletDIDMethod)) + fmt.Printf("Running tests with issuerProfileID=%s issuerDIDMethod=%s walletDIDMethod=%s\n", + tc.issuerProfileID, tc.issuerDIDMethod, tc.walletDIDMethod) credentialConfigs := make([]oidc4ci.CredentialConfiguration, 0) @@ -279,20 +280,29 @@ func doPreAuthCodeFlowTest(t *testing.T) { offerCredentialURL, err := oidc4ciSetup.InitiatePreAuthorizedIssuance(tc.issuerProfileID, credentialConfigs) require.NoError(t, err) - println(offerCredentialURL) + if tc.displayAPIv2 { + // In the current implementation, VCS determines the configuration ID by matching the credential type. + // To test Display API v2, we intentionally switch to the second configuration ID, which shares the same + // credential type as the first configuration but has a different set of display fields. + setCredentialConfigurationIDs(t, &offerCredentialURL, + []string{"UniversityDegreeCredential_ldp_vc_v2", "UniversityDegreeCredential_ldp_vc_v1"}) + } + + fmt.Printf("offerCredentialURL=%s\n", offerCredentialURL) testHelper := helpers.NewCITestHelper(t, tc.walletDIDMethod, tc.walletKeyType) - opts := did.NewResolverOpts() - opts.SetResolverServerURI(didResolverURL) + resolverOpts := did.NewResolverOpts() + resolverOpts.SetResolverServerURI(didResolverURL) - didResolver, err := did.NewResolver(opts) + didResolver, err := did.NewResolver(resolverOpts) require.NoError(t, err) didID, err := testHelper.DIDDoc.ID() require.NoError(t, err) - interactionRequiredArgs := openid4ci.NewIssuerInitiatedInteractionArgs(offerCredentialURL, testHelper.KMS.GetCrypto(), didResolver) + interactionRequiredArgs := openid4ci.NewIssuerInitiatedInteractionArgs(offerCredentialURL, + testHelper.KMS.GetCrypto(), didResolver) interactionOptionalArgs := openid4ci.NewInteractionOpts() interactionOptionalArgs.SetDocumentLoader(&documentLoaderReverseWrapper{DocumentLoader: testutil.DocumentLoader(t)}) @@ -316,6 +326,9 @@ func doPreAuthCodeFlowTest(t *testing.T) { issuerMetadata, err := interaction.IssuerMetadata() require.NoError(t, err) + issuerURI := interaction.IssuerURI() + require.Equal(t, tc.expectedIssuerURI, issuerURI) + offeringDisplayData := display.ResolveCredentialOffer( issuerMetadata, interaction.OfferedCredentialsTypes(), @@ -382,28 +395,28 @@ func doPreAuthCodeFlowTest(t *testing.T) { } } - credentials, err := interaction.RequestCredentialWithPreAuth(vm, opt) - require.NoError(t, err) - require.NotNil(t, credentials) - - for i := 0; i < credentials.Length(); i++ { - cred := credentials.AtIndex(i) + var subjectID string - require.NotEmpty(t, cred.ID()) - require.NotEmpty(t, cred.IssuerID()) - require.NotEmpty(t, cred.Types()) - require.True(t, cred.IssuanceDate() > 0) - require.True(t, cred.ExpirationDate() > 0) + if tc.displayAPIv2 { + subjectID = requestCredentialWithPreAuthV2(t, interaction, vm, didResolver, opt, tc.issuerDIDMethod, + tc.issuerProfileID, tc.expectedDisplayData) + } else { + subjectID = requestCredentialWithPreAuth(t, interaction, vm, didResolver, opt, tc.issuerDIDMethod, + tc.issuerProfileID, tc.expectedDisplayData) } + require.Contains(t, subjectID, didID) + requestedAcknowledgment, err := interaction.Acknowledgment() require.NotNil(t, requestedAcknowledgment) + require.NoError(t, err) requestedAcknowledgmentData, err := requestedAcknowledgment.Serialize() require.NoError(t, err) requestedAcknowledgmentRestored, err := openid4ci.NewAcknowledgment(requestedAcknowledgmentData) require.NotNil(t, requestedAcknowledgmentRestored) + require.NoError(t, err) err = requestedAcknowledgmentRestored.SetInteractionDetails(fmt.Sprintf(`{"profile": %q}`, tc.issuerProfileID)) require.NoError(t, err) @@ -414,36 +427,15 @@ func doPreAuthCodeFlowTest(t *testing.T) { require.NoError(t, requestedAcknowledgmentRestored.Success()) } - vc := credentials.AtIndex(0) - - serializedVC, err := vc.Serialize() - require.NoError(t, err) - - println("credential:", serializedVC) - require.NoError(t, err) - require.Contains(t, vc.VC.Contents().Issuer.ID, tc.issuerDIDMethod) - - helpers.ResolveDisplayData(t, credentials, tc.expectedDisplayData, interaction.IssuerURI(), tc.issuerProfileID, - didResolver) - - issuerURI := interaction.IssuerURI() - require.Equal(t, tc.expectedIssuerURI, issuerURI) - - subID, err := verifiable.SubjectID(vc.VC.Contents().Subject) - require.NoError(t, err) - require.Contains(t, subID, didID) - - // require.NoError(t, vcStatusVerifier.Verify(vc)) - testHelper.CheckActivityLogAfterOpenID4CIFlow(t, vcsAPIDirectURL, - tc.issuerProfileID, subID) + tc.issuerProfileID, subjectID) } require.Len(t, traceIDs, len(preAuthTests)) time.Sleep(5 * time.Second) for _, traceID := range traceIDs { - _, err = testenv.NewHttpRequest().Send(http.MethodGet, + _, err := testenv.NewHttpRequest().Send(http.MethodGet, queryTraceURL+traceID, "", nil, @@ -454,6 +446,115 @@ func doPreAuthCodeFlowTest(t *testing.T) { } } +func setCredentialConfigurationIDs(t *testing.T, credentialOfferURL *string, configIDs []string) { + t.Helper() + + u, err := url.Parse(*credentialOfferURL) + require.NoError(t, err) + + offerParam := u.Query().Get("credential_offer") + require.NotEmpty(t, offerParam) + + var offer map[string]interface{} + err = json.Unmarshal([]byte(offerParam), &offer) + require.NoError(t, err) + + offer["credential_configuration_ids"] = configIDs + + modifiedOffer, err := json.Marshal(offer) + require.NoError(t, err) + + query := u.Query() + query.Set("credential_offer", string(modifiedOffer)) + u.RawQuery = query.Encode() + + *credentialOfferURL = u.String() +} + +func requestCredentialWithPreAuth( + t *testing.T, + interaction *openid4ci.IssuerInitiatedInteraction, + vm *api.VerificationMethod, + didResolver *did.Resolver, + opts *openid4ci.RequestCredentialWithPreAuthOpts, + issuerDIDMethod, issuerProfileID string, + expectedDisplayData *display.Data, +) string { + credentials, err := interaction.RequestCredentialWithPreAuth(vm, opts) + require.NoError(t, err) + require.NotNil(t, credentials) + + for i := 0; i < credentials.Length(); i++ { + cred := credentials.AtIndex(i) + + require.NotEmpty(t, cred.ID()) + require.NotEmpty(t, cred.IssuerID()) + require.NotEmpty(t, cred.Types()) + require.True(t, cred.IssuanceDate() > 0) + require.True(t, cred.ExpirationDate() > 0) + } + + vc := credentials.AtIndex(0) + + serializedVC, err := vc.Serialize() + require.NoError(t, err) + + fmt.Printf("credential: %s\n", serializedVC) + + require.NoError(t, err) + require.Contains(t, vc.VC.Contents().Issuer.ID, issuerDIDMethod) + + helpers.ResolveDisplayData(t, credentials, expectedDisplayData, interaction.IssuerURI(), issuerProfileID, + didResolver) + + subjectID, err := verifiable.SubjectID(vc.VC.Contents().Subject) + require.NoError(t, err) + + return subjectID +} + +func requestCredentialWithPreAuthV2( + t *testing.T, + interaction *openid4ci.IssuerInitiatedInteraction, + vm *api.VerificationMethod, + didResolver *did.Resolver, + opts *openid4ci.RequestCredentialWithPreAuthOpts, + issuerDIDMethod, issuerProfileID string, + expectedDisplayData *display.Data, +) string { + credentials, err := interaction.RequestCredentialWithPreAuthV2(vm, opts) + require.NoError(t, err) + require.NotNil(t, credentials) + + for i := 0; i < credentials.Length(); i++ { + cred := credentials.AtIndex(i) + + require.NotEmpty(t, cred.ID()) + require.NotEmpty(t, cred.IssuerID()) + require.NotEmpty(t, cred.Types()) + require.True(t, cred.IssuanceDate() > 0) + require.True(t, cred.ExpirationDate() > 0) + } + + vc := credentials.AtIndex(0) + + serializedVC, err := vc.Serialize() + require.NoError(t, err) + + fmt.Printf("credential: %s\n", serializedVC) + + require.NoError(t, err) + require.Contains(t, vc.VC.Contents().Issuer.ID, issuerDIDMethod) + + helpers.ResolveDisplayDataV2(t, credentials, expectedDisplayData, interaction.IssuerURI(), issuerProfileID, + didResolver) + + subjectID, err := verifiable.SubjectID(vc.VC.Contents().Subject) + require.NoError(t, err) + + return subjectID +} + func doAuthCodeFlowTest(t *testing.T, useDynamicClientRegistration bool) { credentialOfferURL, err := oidc4ci.InitiateAuthCodeIssuance() require.NoError(t, err) diff --git a/test/integration/pkg/helpers/displaydata.go b/test/integration/pkg/helpers/displaydata.go index b923af06..d812f9d7 100644 --- a/test/integration/pkg/helpers/displaydata.go +++ b/test/integration/pkg/helpers/displaydata.go @@ -73,6 +73,59 @@ func CheckResolvedDisplayData(t *testing.T, actualDisplayData, expectedDisplayDa checkCredentialDisplay(t, actualCredentialDisplay, expectedCredentialDisplay, checkClaims) } +func ResolveDisplayDataV2(t *testing.T, credentials *verifiable.CredentialsArrayV2, expectedDisplayData *display.Data, + issuerURI, issuerProfileID string, didResolver *did.Resolver, +) { + metricsLogger := metricslogger.NewMetricsLogger() + + opts := display.NewOpts() + opts.SetMetricsLogger(metricsLogger) + opts.DisableHTTPClientTLSVerify() + opts.SetDIDResolver(didResolver) + + resolvedDisplayData, err := display.ResolveCredentialV2(credentials, issuerURI, opts) + require.NoError(t, err) + require.NotNil(t, resolvedDisplayData) + + CheckResolvedDisplayDataV2(t, resolvedDisplayData, expectedDisplayData) + + checkResolveMetricsEvent(t, metricsLogger, issuerProfileID) +} + +func CheckResolvedDisplayDataV2(t *testing.T, resolvedDisplayData *display.Resolved, expectedDisplayData *display.Data) { + t.Helper() + + require.Equal(t, 1, resolvedDisplayData.LocalizedIssuersLength()) + + resolvedIssuerData := resolvedDisplayData.LocalizedIssuerAtIndex(0) + expectedIssuerData := expectedDisplayData.IssuerDisplay() + + require.Equal(t, expectedIssuerData.Name(), resolvedIssuerData.Name()) + require.Equal(t, expectedIssuerData.Locale(), resolvedIssuerData.Locale()) + require.Equal(t, expectedIssuerData.BackgroundColor(), resolvedIssuerData.BackgroundColor()) + require.Equal(t, expectedIssuerData.TextColor(), resolvedIssuerData.TextColor()) + + require.Equal(t, expectedDisplayData.CredentialDisplaysLength(), resolvedDisplayData.CredentialsLength()) + + for i := 0; i < expectedDisplayData.CredentialDisplaysLength(); i++ { + expectedCredentialDisplay := expectedDisplayData.CredentialDisplayAtIndex(i) + resolvedCredentialDisplay := resolvedDisplayData.CredentialAtIndex(i) + + require.Equal(t, resolvedCredentialDisplay.LocalizedOverviewsLength(), 1) + + expectedOverview := expectedCredentialDisplay.Overview() + resolvedOverview := resolvedCredentialDisplay.LocalizedOverviewAtIndex(0) + + require.Equal(t, expectedOverview.Name(), resolvedOverview.Name()) + require.Equal(t, expectedOverview.Locale(), resolvedOverview.Locale()) + require.Equal(t, expectedOverview.BackgroundColor(), resolvedOverview.BackgroundColor()) + require.Equal(t, expectedOverview.TextColor(), resolvedOverview.TextColor()) + require.Equal(t, expectedOverview.Logo(), resolvedOverview.Logo()) + + require.Equal(t, expectedCredentialDisplay.ClaimsLength(), resolvedCredentialDisplay.SubjectsLength()) + } +} + func claimsMap(credentialDisplay *display.CredentialDisplay) map[string]string { m := make(map[string]string)