diff --git a/go.sum b/go.sum index 3b46f6cb1e..9ff15d6820 100644 --- a/go.sum +++ b/go.sum @@ -199,6 +199,7 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/jsonld/scalar.go b/jsonld/scalar.go index fca2d7453c..e13ec6aa7b 100644 --- a/jsonld/scalar.go +++ b/jsonld/scalar.go @@ -29,8 +29,6 @@ type Scalar interface { fmt.Stringer // Value returns the underlying value (string, float, true or false) Value() interface{} - // Equal compares the value of both Scalar - Equal(o Scalar) bool } // StringScalar is the string version of a Scalar @@ -44,11 +42,6 @@ func (ss StringScalar) Value() interface{} { return string(ss) } -func (ss StringScalar) Equal(o Scalar) bool { - v, ok := o.(StringScalar) - return ok && v == ss -} - // BoolScalar is the boolean version of a Scalar type BoolScalar bool @@ -63,11 +56,6 @@ func (bs BoolScalar) Value() interface{} { return bool(bs) } -func (bs BoolScalar) Equal(o Scalar) bool { - v, ok := o.(BoolScalar) - return ok && v == bs -} - // Float64Scalar is the float64 version of a Scalar type Float64Scalar float64 @@ -79,11 +67,6 @@ func (fs Float64Scalar) Value() interface{} { return float64(fs) } -func (fs Float64Scalar) Equal(o Scalar) bool { - v, ok := o.(Float64Scalar) - return ok && v == fs -} - // ErrInvalidValue is returned when an invalid value is parsed var ErrInvalidValue = errors.New("invalid value") diff --git a/jsonld/scalar_test.go b/jsonld/scalar_test.go index 4b478d91b4..38d88228e5 100644 --- a/jsonld/scalar_test.go +++ b/jsonld/scalar_test.go @@ -93,28 +93,3 @@ func TestScalar_String(t *testing.T) { assert.Equal(t, "false", s.String()) }) } - -func TestBoolScalar_Equal(t *testing.T) { - type entry struct { - a Scalar - b Scalar - equal bool - } - - tt := []entry{ - {a: StringScalar("string"), b: StringScalar("string"), equal: true}, - {a: StringScalar("string"), b: StringScalar("STRING"), equal: false}, - {a: StringScalar("1"), b: Float64Scalar(1), equal: false}, - - {a: BoolScalar(true), b: BoolScalar(true), equal: true}, - {a: BoolScalar(true), b: BoolScalar(false), equal: false}, - {a: BoolScalar(true), b: StringScalar("true"), equal: false}, - - {a: Float64Scalar(1.5), b: Float64Scalar(1.5), equal: true}, - {a: Float64Scalar(1.5), b: Float64Scalar(1), equal: false}, - {a: Float64Scalar(1.5), b: StringScalar("1.5"), equal: false}, - } - for i, test := range tt { - assert.Equal(t, test.a.Equal(test.b), test.equal, "test %d failed: a=%s, b=%s, expected=%t", i, test.a, test.b, test.equal) - } -} diff --git a/vcr/api/oidc4vci/v0/holder_test.go b/vcr/api/oidc4vci/v0/holder_test.go index 0fb515d4a5..9e094841fc 100644 --- a/vcr/api/oidc4vci/v0/holder_test.go +++ b/vcr/api/oidc4vci/v0/holder_test.go @@ -21,6 +21,7 @@ package v0 import ( "context" "encoding/json" + ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/holder" @@ -81,12 +82,12 @@ func TestWrapper_HandleCredentialOffer(t *testing.T) { credentialOffer := oidc4vci.CredentialOffer{ CredentialIssuer: issuerDID.String(), - Credentials: []map[string]interface{}{ + Credentials: []oidc4vci.OfferedCredential{ { - "format": oidc4vci.VerifiableCredentialJSONLDFormat, - "credential_definition": map[string]interface{}{ - "@context": []string{"a", "b"}, - "type": []string{"VerifiableCredential", "HumanCredential"}, + Format: oidc4vci.VerifiableCredentialJSONLDFormat, + CredentialDefinition: &oidc4vci.CredentialDefinition{ + Context: []ssi.URI{ssi.MustParseURI("a"), ssi.MustParseURI("b")}, + Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI("HumanCredential")}, }, }, }, diff --git a/vcr/api/oidc4vci/v0/issuer_test.go b/vcr/api/oidc4vci/v0/issuer_test.go index 192b4b13ef..5e44e44fd9 100644 --- a/vcr/api/oidc4vci/v0/issuer_test.go +++ b/vcr/api/oidc4vci/v0/issuer_test.go @@ -173,7 +173,7 @@ func TestWrapper_RequestCredential(t *testing.T) { }, Body: &RequestCredentialJSONRequestBody{ Format: "ldp_vc", - CredentialDefinition: &map[string]interface{}{}, + CredentialDefinition: &oidc4vci.CredentialDefinition{}, Proof: nil, }, }) diff --git a/vcr/holder/openid.go b/vcr/holder/openid.go index ac616a6ad4..86dc4053e2 100644 --- a/vcr/holder/openid.go +++ b/vcr/holder/openid.go @@ -50,7 +50,7 @@ var nowFunc = time.Now var _ OpenIDHandler = (*openidHandler)(nil) // NewOpenIDHandler creates an OpenIDHandler that tries to retrieve offered credentials, to store it in the given credential store. -func NewOpenIDHandler(config oidc4vci.ClientConfig, did did.DID, identifier string, credentialStore vcrTypes.Writer, signer crypto.JWTSigner, resolver vdr.KeyResolver, jsonldReader jsonld.Reader) OpenIDHandler { +func NewOpenIDHandler(config oidc4vci.ClientConfig, did did.DID, identifier string, credentialStore vcrTypes.Writer, signer crypto.JWTSigner, resolver vdr.KeyResolver) OpenIDHandler { return &openidHandler{ did: did, identifier: identifier, @@ -59,7 +59,6 @@ func NewOpenIDHandler(config oidc4vci.ClientConfig, did did.DID, identifier stri resolver: resolver, config: config, issuerClientCreator: oidc4vci.NewIssuerAPIClient, - jsonldReader: jsonldReader, } } @@ -94,6 +93,21 @@ func (h openidHandler) HandleCredentialOffer(ctx context.Context, offer oidc4vci StatusCode: http.StatusBadRequest, } } + offeredCredential := offer.Credentials[0] + if offeredCredential.Format != oidc4vci.VerifiableCredentialJSONLDFormat { + return oidc4vci.Error{ + Err: fmt.Errorf("credential offer: unsupported format '%s'", offeredCredential.Format), + Code: oidc4vci.UnsupportedCredentialType, + StatusCode: http.StatusBadRequest, + } + } + if err := offeredCredential.CredentialDefinition.Validate(true); err != nil { + return oidc4vci.Error{ + Err: fmt.Errorf("credential offer: %w", err), + Code: oidc4vci.InvalidRequest, + StatusCode: http.StatusBadRequest, + } + } preAuthorizedCode := getPreAuthorizedCodeFromOffer(offer) if preAuthorizedCode == "" { @@ -149,7 +163,7 @@ func (h openidHandler) HandleCredentialOffer(ctx context.Context, offer oidc4vci retrieveCtx := audit.Context(ctx, "app-openid4vci", "VCR/OpenID4VCI", "RetrieveCredential") retrieveCtx, cancel := context.WithTimeout(retrieveCtx, h.config.Timeout) defer cancel() - credential, err := h.retrieveCredential(retrieveCtx, issuerClient, offer, accessTokenResponse) + credential, err := h.retrieveCredential(retrieveCtx, issuerClient, offeredCredential.CredentialDefinition, accessTokenResponse) if err != nil { return oidc4vci.Error{ Err: fmt.Errorf("unable to retrieve credential: %w", err), @@ -157,10 +171,10 @@ func (h openidHandler) HandleCredentialOffer(ctx context.Context, offer oidc4vci StatusCode: http.StatusInternalServerError, } } - if err = credentialTypesMatchOffer(h.jsonldReader, *credential, offer.Credentials[0]); err != nil { + if err = oidc4vci.ValidateDefinitionWithCredential(*credential, *offeredCredential.CredentialDefinition); err != nil { return oidc4vci.Error{ Err: fmt.Errorf("received credential does not match offer: %w", err), - Code: oidc4vci.UnsupportedCredentialType, + Code: oidc4vci.InvalidRequest, StatusCode: http.StatusInternalServerError, } } @@ -174,48 +188,6 @@ func (h openidHandler) HandleCredentialOffer(ctx context.Context, offer oidc4vci return nil } -// credentialTypesMatchOffer performs format specific validation. -func credentialTypesMatchOffer(reader jsonld.Reader, credential vc.VerifiableCredential, offeredCredential map[string]interface{}) error { - switch offeredCredential["format"] { - case oidc4vci.VerifiableCredentialJSONLDFormat: - // In json-LD format the types need to be compared in expanded format - document, err := reader.Read(offeredCredential["credential_definition"]) - if err != nil { - return fmt.Errorf("invalid credential_definition in offer: %w", err) - } - // TODO: can credentialDefinition contain invalid values that makes this panic? - expectedTypes := document.ValueAt(jsonld.NewPath("@type")) - - document, err = reader.Read(credential) - if err != nil { - return fmt.Errorf("invalid credential: %w", err) - } - receivedTypes := document.ValueAt(jsonld.NewPath("@type")) - - if !equal(expectedTypes, receivedTypes) { - return errors.New("credential Type do not match") - } - - return nil - default: - return errors.New("unsupported credential format") - } -} - -// equal returns true if both slices have the same values in the same order. -// Note: JSON arrays are ordered, JSON object elements are not. -func equal(a, b []jsonld.Scalar) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if !a[i].Equal(b[i]) { - return false - } - } - return true -} - func getPreAuthorizedCodeFromOffer(offer oidc4vci.CredentialOffer) string { params, ok := offer.Grants[oidc4vci.PreAuthorizedCodeGrant].(map[string]interface{}) if !ok { @@ -228,7 +200,7 @@ func getPreAuthorizedCodeFromOffer(offer oidc4vci.CredentialOffer) string { return preAuthorizedCode } -func (h openidHandler) retrieveCredential(ctx context.Context, issuerClient oidc4vci.IssuerAPIClient, offer oidc4vci.CredentialOffer, tokenResponse *oidc4vci.TokenResponse) (*vc.VerifiableCredential, error) { +func (h openidHandler) retrieveCredential(ctx context.Context, issuerClient oidc4vci.IssuerAPIClient, offer *oidc4vci.CredentialDefinition, tokenResponse *oidc4vci.TokenResponse) (*vc.VerifiableCredential, error) { keyID, err := h.resolver.ResolveSigningKeyID(h.did, nil) headers := map[string]interface{}{ "typ": oidc4vci.JWTTypeOpenID4VCIProof, // MUST be openid4vci-proof+jwt, which explicitly types the proof JWT as recommended in Section 3.11 of [RFC8725]. @@ -246,7 +218,7 @@ func (h openidHandler) retrieveCredential(ctx context.Context, issuerClient oidc } credentialRequest := oidc4vci.CredentialRequest{ - CredentialDefinition: &offer.Credentials[0], + CredentialDefinition: offer, Format: oidc4vci.VerifiableCredentialJSONLDFormat, Proof: &oidc4vci.CredentialRequestProof{ Jwt: proof, diff --git a/vcr/holder/openid_test.go b/vcr/holder/openid_test.go index 3b0ece5906..90f11e5ae5 100644 --- a/vcr/holder/openid_test.go +++ b/vcr/holder/openid_test.go @@ -27,7 +27,6 @@ import ( "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto" - "github.com/nuts-foundation/nuts-node/jsonld" "github.com/nuts-foundation/nuts-node/vcr/oidc4vci" "github.com/nuts-foundation/nuts-node/vcr/types" vdrTypes "github.com/nuts-foundation/nuts-node/vdr/types" @@ -43,12 +42,12 @@ var holderDID = did.MustParseDID("did:nuts:holder") var issuerDID = did.MustParseDID("did:nuts:issuer") func TestNewOIDCWallet(t *testing.T) { - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil, jsonld.Reader{}) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil) assert.NotNil(t, w) } func Test_wallet_Metadata(t *testing.T) { - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil, jsonld.Reader{}) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil) metadata := w.Metadata() @@ -60,18 +59,7 @@ func Test_wallet_Metadata(t *testing.T) { func Test_wallet_HandleCredentialOffer(t *testing.T) { credentialOffer := oidc4vci.CredentialOffer{ CredentialIssuer: issuerDID.String(), - Credentials: []map[string]interface{}{ - { - "format": oidc4vci.VerifiableCredentialJSONLDFormat, - "credential_definition": map[string]interface{}{ - "@context": []string{ - "https://www.w3.org/2018/credentials/v1", - "http://example.org/credentials/V1", - }, - "type": []string{"VerifiableCredential", "HumanCredential"}, - }, - }, - }, + Credentials: offeredCredential(), Grants: map[string]interface{}{ "some-other-grant": map[string]interface{}{}, "urn:ietf:params:oauth:grant-type:pre-authorized_code": map[string]interface{}{ @@ -83,7 +71,6 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { CredentialIssuer: issuerDID.String(), CredentialEndpoint: "credential-endpoint", } - jsonldReader := jsonld.Reader{DocumentLoader: jsonld.NewTestJSONLDManager(t).DocumentLoader()} t.Run("ok", func(t *testing.T) { ctrl := gomock.NewController(t) nonce := "nonsens" @@ -117,7 +104,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { return time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) } - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", credentialStore, jwtSigner, keyResolver, jsonldReader).(*openidHandler) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", credentialStore, jwtSigner, keyResolver).(*openidHandler) w.issuerClientCreator = func(_ context.Context, httpClient core.HTTPRequestDoer, credentialIssuerIdentifier string) (oidc4vci.IssuerAPIClient, error) { return issuerAPIClient, nil } @@ -129,15 +116,15 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { require.NoError(t, err) }) t.Run("pre-authorized code grant", func(t *testing.T) { - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil, jsonldReader).(*openidHandler) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil).(*openidHandler) t.Run("no grants", func(t *testing.T) { - offer := oidc4vci.CredentialOffer{Credentials: emptyOfferedCredential()} + offer := oidc4vci.CredentialOffer{Credentials: offeredCredential()} err := w.HandleCredentialOffer(audit.TestContext(), offer) require.EqualError(t, err, "invalid_grant - couldn't find (valid) pre-authorized code grant in credential offer") }) t.Run("no pre-authorized grant", func(t *testing.T) { offer := oidc4vci.CredentialOffer{ - Credentials: emptyOfferedCredential(), + Credentials: offeredCredential(), Grants: map[string]interface{}{ "some-other-grant": nil, }, @@ -147,7 +134,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { }) t.Run("invalid pre-authorized grant", func(t *testing.T) { offer := oidc4vci.CredentialOffer{ - Credentials: emptyOfferedCredential(), + Credentials: offeredCredential(), Grants: map[string]interface{}{ "urn:ietf:params:oauth:grant-type:pre-authorized_code": map[string]interface{}{ "pre-authorized_code": nil, @@ -159,24 +146,12 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { }) }) t.Run("error - too many credentials in offer", func(t *testing.T) { - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil, jsonldReader) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil) offer := oidc4vci.CredentialOffer{ - Credentials: []map[string]interface{}{ - { - "format": oidc4vci.VerifiableCredentialJSONLDFormat, - "credential_definition": map[string]interface{}{ - "@context": []string{"a", "b"}, - "type": []string{"VerifiableCredential", "HumanCredential"}, - }, - }, - { - "format": oidc4vci.VerifiableCredentialJSONLDFormat, - "credential_definition": map[string]interface{}{ - "@context": []string{"a", "b"}, - "type": []string{"VerifiableCredential", "HumanCredential"}, - }, - }, + Credentials: []oidc4vci.OfferedCredential{ + offeredCredential()[0], + offeredCredential()[0], }, } err := w.HandleCredentialOffer(audit.TestContext(), offer).(oidc4vci.Error) @@ -189,7 +164,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { issuerAPIClient := oidc4vci.NewMockIssuerAPIClient(ctrl) issuerAPIClient.EXPECT().RequestAccessToken(gomock.Any(), gomock.Any()).Return(nil, errors.New("request failed")) - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil, jsonldReader).(*openidHandler) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil).(*openidHandler) w.issuerClientCreator = func(_ context.Context, httpClient core.HTTPRequestDoer, credentialIssuerIdentifier string) (oidc4vci.IssuerAPIClient, error) { return issuerAPIClient, nil } @@ -203,7 +178,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { issuerAPIClient := oidc4vci.NewMockIssuerAPIClient(ctrl) issuerAPIClient.EXPECT().RequestAccessToken(gomock.Any(), gomock.Any()).Return(&oidc4vci.TokenResponse{}, nil) - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil, jsonldReader).(*openidHandler) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil).(*openidHandler) w.issuerClientCreator = func(_ context.Context, httpClient core.HTTPRequestDoer, credentialIssuerIdentifier string) (oidc4vci.IssuerAPIClient, error) { return issuerAPIClient, nil } @@ -217,7 +192,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { issuerAPIClient := oidc4vci.NewMockIssuerAPIClient(ctrl) issuerAPIClient.EXPECT().RequestAccessToken(gomock.Any(), gomock.Any()).Return(&oidc4vci.TokenResponse{AccessToken: "foo"}, nil) - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil, jsonldReader).(*openidHandler) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil).(*openidHandler) w.issuerClientCreator = func(_ context.Context, httpClient core.HTTPRequestDoer, credentialIssuerIdentifier string) (oidc4vci.IssuerAPIClient, error) { return issuerAPIClient, nil } @@ -227,7 +202,7 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { require.EqualError(t, err, "invalid_token - c_nonce is missing") }) t.Run("error - no credentials in offer", func(t *testing.T) { - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil, jsonldReader) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil) err := w.HandleCredentialOffer(audit.TestContext(), oidc4vci.CredentialOffer{}).(oidc4vci.Error) @@ -235,15 +210,11 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { assert.Equal(t, http.StatusBadRequest, err.StatusCode) }) t.Run("error - can't issuer client (metadata can't be loaded)", func(t *testing.T) { - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil, jsonldReader) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil) err := w.HandleCredentialOffer(audit.TestContext(), oidc4vci.CredentialOffer{ CredentialIssuer: "http://localhost:87632", - Credentials: []map[string]interface{}{ - { - "format": oidc4vci.VerifiableCredentialJSONLDFormat, - }, - }, + Credentials: offeredCredential(), Grants: map[string]interface{}{ "urn:ietf:params:oauth:grant-type:pre-authorized_code": map[string]interface{}{ "pre-authorized_code": "foo", @@ -254,13 +225,14 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { assert.EqualError(t, err, "server_error - unable to create issuer client: unable to load Credential Issuer Metadata (identifier=http://localhost:87632): "+ "http request error: Get \"http://localhost:87632/.well-known/openid-credential-issuer\": dial tcp: address 87632: invalid port") }) - t.Run("error - types do not match", func(t *testing.T) { + t.Run("error - credential does not match offer", func(t *testing.T) { + offer := offeredCredential()[0] ctrl := gomock.NewController(t) issuerAPIClient := oidc4vci.NewMockIssuerAPIClient(ctrl) issuerAPIClient.EXPECT().Metadata().Return(metadata) issuerAPIClient.EXPECT().RequestAccessToken(gomock.Any(), gomock.Any()).Return(&oidc4vci.TokenResponse{AccessToken: "access-token", CNonce: "c_nonce"}, nil) issuerAPIClient.EXPECT().RequestCredential(gomock.Any(), gomock.Any(), gomock.Any()).Return(&vc.VerifiableCredential{ - Context: []ssi.URI{ssi.MustParseURI("https://www.w3.org/2018/credentials/v1")}, + Context: offer.CredentialDefinition.Context, Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential")}, }, nil) jwtSigner := crypto.NewMockJWTSigner(ctrl) @@ -268,64 +240,50 @@ func Test_wallet_HandleCredentialOffer(t *testing.T) { keyResolver := vdrTypes.NewMockKeyResolver(ctrl) keyResolver.EXPECT().ResolveSigningKeyID(holderDID, nil) - w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, jwtSigner, keyResolver, jsonldReader).(*openidHandler) + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, jwtSigner, keyResolver).(*openidHandler) w.issuerClientCreator = func(_ context.Context, _ core.HTTPRequestDoer, _ string) (oidc4vci.IssuerAPIClient, error) { return issuerAPIClient, nil } err := w.HandleCredentialOffer(audit.TestContext(), credentialOffer) - require.EqualError(t, err, "unsupported_credential_type - received credential does not match offer: credential Type do not match") + require.EqualError(t, err, "invalid_request - received credential does not match offer: credential does not match credential_definition: type mismatch") }) -} + t.Run("error - unsupported format", func(t *testing.T) { + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil) -func Test_credentialTypesMatchOffer(t *testing.T) { - offer := map[string]any{ - "format": oidc4vci.VerifiableCredentialJSONLDFormat, - "credential_definition": map[string]interface{}{ - "@context": []string{ - "https://www.w3.org/2018/credentials/v1", - "http://example.org/credentials/V1", - }, - "type": []string{"VerifiableCredential", "HumanCredential"}, - }, - } - credential := vc.VerifiableCredential{ - Context: []ssi.URI{ssi.MustParseURI("https://www.w3.org/2018/credentials/v1"), ssi.MustParseURI("http://example.org/credentials/V1")}, - Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI("HumanCredential")}, - } - jsonldReader := jsonld.Reader{DocumentLoader: jsonld.NewTestJSONLDManager(t).DocumentLoader()} + err := w.HandleCredentialOffer(audit.TestContext(), oidc4vci.CredentialOffer{ + Credentials: []oidc4vci.OfferedCredential{{Format: "not supported"}}, + }).(oidc4vci.Error) - t.Run("ok", func(t *testing.T) { - assert.NoError(t, credentialTypesMatchOffer(jsonldReader, credential, offer)) - }) - t.Run("error - unsupported credential format", func(t *testing.T) { - err := credentialTypesMatchOffer(jsonldReader, vc.VerifiableCredential{}, map[string]interface{}{}) - assert.EqualError(t, err, "unsupported credential format") - }) - t.Run("error - invalid credential_definition", func(t *testing.T) { - err := credentialTypesMatchOffer(jsonldReader, vc.VerifiableCredential{}, - map[string]interface{}{ - "format": oidc4vci.VerifiableCredentialJSONLDFormat, - "credential_definition": "", - }) - assert.EqualError(t, err, "invalid credential_definition in offer: json: cannot unmarshal string into Go value of type map[string]interface {}") - }) - t.Run("error - invalid credential", func(t *testing.T) { - err := credentialTypesMatchOffer(jsonldReader, vc.VerifiableCredential{}, offer) - assert.EqualError(t, err, "invalid credential: invalid property: Dropping property that did not expand into an absolute IRI or keyword.") + assert.EqualError(t, err, "unsupported_credential_type - credential offer: unsupported format 'not supported'") + assert.Equal(t, http.StatusBadRequest, err.StatusCode) }) - t.Run("error - types do not match", func(t *testing.T) { - c := credential - c.Type[0], c.Type[1] = c.Type[1], c.Type[0] - defer func() { c.Type[0], c.Type[1] = c.Type[1], c.Type[0] }() - err := credentialTypesMatchOffer(jsonldReader, credential, offer) - assert.EqualError(t, err, "credential Type do not match") + t.Run("error - credentialSubject not allowed in offer", func(t *testing.T) { + w := NewOpenIDHandler(oidc4vci.ClientConfig{}, holderDID, "https://holder.example.com", nil, nil, nil) + credentials := offeredCredential() + credentials[0].CredentialDefinition.CredentialSubject = new(map[string]interface{}) + + err := w.HandleCredentialOffer(audit.TestContext(), oidc4vci.CredentialOffer{Credentials: credentials}).(oidc4vci.Error) + + assert.EqualError(t, err, "invalid_request - credential offer: invalid credential_definition: credentialSubject not allowed in offer") + assert.Equal(t, http.StatusBadRequest, err.StatusCode) }) } -// emptyOfferedCredential returns a structure that can be used as CredentialOffer.Credentials, -// specifying an offer with a single credential without properties (which is invalid, but required to pass basic validation). -func emptyOfferedCredential() []map[string]interface{} { - return []map[string]interface{}{{"format": oidc4vci.VerifiableCredentialJSONLDFormat}} +// offeredCredential returns a structure that can be used as CredentialOffer.Credentials, +func offeredCredential() []oidc4vci.OfferedCredential { + return []oidc4vci.OfferedCredential{{ + Format: oidc4vci.VerifiableCredentialJSONLDFormat, + CredentialDefinition: &oidc4vci.CredentialDefinition{ + Context: []ssi.URI{ + ssi.MustParseURI("https://www.w3.org/2018/credentials/v1"), + ssi.MustParseURI("http://example.org/credentials/V1"), + }, + Type: []ssi.URI{ + ssi.MustParseURI("VerifiableCredential"), + ssi.MustParseURI("HumanCredential"), + }, + }, + }} } diff --git a/vcr/issuer/openid.go b/vcr/issuer/openid.go index 07d2a8f256..2c9c01d6c2 100644 --- a/vcr/issuer/openid.go +++ b/vcr/issuer/openid.go @@ -228,8 +228,20 @@ func (i *openidHandler) OfferCredential(ctx context.Context, credential vc.Verif } func (i *openidHandler) HandleCredentialRequest(ctx context.Context, request oidc4vci.CredentialRequest, accessToken string) (*vc.VerifiableCredential, error) { - // TODO: Verify requested format and credential definition - // See https://github.com/nuts-foundation/nuts-node/issues/2037 + if request.Format != oidc4vci.VerifiableCredentialJSONLDFormat { + return nil, oidc4vci.Error{ + Err: fmt.Errorf("credential request: unsupported format '%s'", request.Format), + Code: oidc4vci.UnsupportedCredentialType, + StatusCode: http.StatusBadRequest, + } + } + if err := request.CredentialDefinition.Validate(false); err != nil { + return nil, oidc4vci.Error{ + Err: fmt.Errorf("credential request: %w", err), + Code: oidc4vci.InvalidRequest, + StatusCode: http.StatusBadRequest, + } + } flow, err := i.store.FindByReference(ctx, accessTokenRefType, accessToken) if err != nil { return nil, err @@ -259,6 +271,14 @@ func (i *openidHandler) HandleCredentialRequest(ctx context.Context, request oid return nil, err } + if err = oidc4vci.ValidateDefinitionWithCredential(credential, *request.CredentialDefinition); err != nil { + return nil, oidc4vci.Error{ + Err: fmt.Errorf("requested credential does not match offer: %w", err), + Code: oidc4vci.InvalidRequest, + StatusCode: http.StatusBadRequest, + } + } + // Important: since we (for now) create the VC even before the wallet requests it, we don't know if every VC is actually retrieved by the wallet. // This is a temporary shortcut, since changing that requires a lot of refactoring. // To make actually retrieved VC traceable, we log it to the audit log. @@ -408,11 +428,11 @@ func (i *openidHandler) createOffer(ctx context.Context, credential vc.Verifiabl } offer := oidc4vci.CredentialOffer{ CredentialIssuer: i.issuerIdentifierURL, - Credentials: []map[string]interface{}{{ - "format": oidc4vci.VerifiableCredentialJSONLDFormat, - "credential_definition": map[string]interface{}{ - "@context": credential.Context, - "type": credential.Type, + Credentials: []oidc4vci.OfferedCredential{{ + Format: oidc4vci.VerifiableCredentialJSONLDFormat, + CredentialDefinition: &oidc4vci.CredentialDefinition{ + Context: credential.Context, + Type: credential.Type, }, }}, Grants: map[string]interface{}{ diff --git a/vcr/issuer/openid_test.go b/vcr/issuer/openid_test.go index ae98fd58d5..002cc8e907 100644 --- a/vcr/issuer/openid_test.go +++ b/vcr/issuer/openid_test.go @@ -22,6 +22,7 @@ import ( "context" crypt "crypto" "errors" + ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/audit" @@ -52,6 +53,14 @@ var issuedVC = vc.VerifiableCredential{ "id": holderDID.String(), }, }, + Context: []ssi.URI{ + ssi.MustParseURI("https://www.w3.org/2018/credentials/v1"), + ssi.MustParseURI("http://example.org/credentials/V1"), + }, + Type: []ssi.URI{ + ssi.MustParseURI("VerifiableCredential"), + ssi.MustParseURI("HumanCredential"), + }, } func TestNew(t *testing.T) { @@ -134,6 +143,16 @@ func Test_memoryIssuer_HandleCredentialRequest(t *testing.T) { require.NoError(t, err) return oidc4vci.CredentialRequest{ Format: oidc4vci.VerifiableCredentialJSONLDFormat, + CredentialDefinition: &oidc4vci.CredentialDefinition{ + Context: []ssi.URI{ + ssi.MustParseURI("https://www.w3.org/2018/credentials/v1"), + ssi.MustParseURI("http://example.org/credentials/V1"), + }, + Type: []ssi.URI{ + ssi.MustParseURI("VerifiableCredential"), + ssi.MustParseURI("HumanCredential"), + }, + }, Proof: &oidc4vci.CredentialRequestProof{ Jwt: proof, ProofType: oidc4vci.ProofTypeJWT, @@ -159,6 +178,24 @@ func Test_memoryIssuer_HandleCredentialRequest(t *testing.T) { assert.Equal(t, issuerDID.URI(), response.Issuer) auditLogs.AssertContains(t, "VCR", "VerifiableCredentialRetrievedEvent", audit.TestActor, "VC retrieved by wallet over OIDC4VCI") }) + t.Run("unsupported format", func(t *testing.T) { + request := createRequest(createHeaders(), createClaims(cNonce)) + request.Format = "unsupported format" + + response, err := service.HandleCredentialRequest(ctx, request, accessToken) + + assert.Nil(t, response) + assert.EqualError(t, err, "unsupported_credential_type - credential request: unsupported format 'unsupported format'") + }) + t.Run("invalid credential_definition", func(t *testing.T) { + request := createRequest(createHeaders(), createClaims(cNonce)) + request.CredentialDefinition.Type = []ssi.URI{} + + response, err := service.HandleCredentialRequest(ctx, request, accessToken) + + assert.Nil(t, response) + assert.EqualError(t, err, "invalid_request - credential request: invalid credential_definition: missing type field") + }) t.Run("proof validation", func(t *testing.T) { t.Run("unsupported proof type", func(t *testing.T) { invalidRequest := createRequest(createHeaders(), createClaims("")) @@ -293,6 +330,17 @@ func Test_memoryIssuer_HandleCredentialRequest(t *testing.T) { assertProtocolError(t, err, http.StatusBadRequest, "invalid_proof - nonce not valid for access token") assert.Nil(t, response) }) + t.Run("request does not match offer", func(t *testing.T) { + request := createRequest(createHeaders(), createClaims(cNonce)) + request.CredentialDefinition.Type = []ssi.URI{ + ssi.MustParseURI("DifferentCredential"), + } + + response, err := service.HandleCredentialRequest(ctx, request, accessToken) + + assert.Nil(t, response) + assert.EqualError(t, err, "invalid_request - requested credential does not match offer: credential does not match credential_definition: type mismatch") + }) }) t.Run("unknown access token", func(t *testing.T) { diff --git a/vcr/oidc4vci/issuer_client_test.go b/vcr/oidc4vci/issuer_client_test.go index d0403eedea..a4fb819449 100644 --- a/vcr/oidc4vci/issuer_client_test.go +++ b/vcr/oidc4vci/issuer_client_test.go @@ -88,10 +88,8 @@ func Test_httpIssuerClient_RequestCredential(t *testing.T) { ctx := context.Background() httpClient := &http.Client{} credentialRequest := CredentialRequest{ - CredentialDefinition: &map[string]interface{}{ - "issuer": "issuer", - }, - Format: VerifiableCredentialJSONLDFormat, + CredentialDefinition: &CredentialDefinition{}, + Format: VerifiableCredentialJSONLDFormat, } t.Run("ok", func(t *testing.T) { setup := setupClientTest(t) diff --git a/vcr/oidc4vci/types.go b/vcr/oidc4vci/types.go index 4b4de28c81..9fb295c573 100644 --- a/vcr/oidc4vci/types.go +++ b/vcr/oidc4vci/types.go @@ -22,6 +22,7 @@ package oidc4vci import ( "crypto/tls" + ssi "github.com/nuts-foundation/go-did" "time" ) @@ -97,11 +98,29 @@ type CredentialOffer struct { // CredentialIssuer defines the identifier of the credential issuer. CredentialIssuer string `json:"credential_issuer"` // Credentials defines the credentials offered by the issuer to the wallet. - Credentials []map[string]interface{} `json:"credentials"` + Credentials []OfferedCredential `json:"credentials"` // Grants defines the grants offered by the issuer to the wallet. Grants map[string]interface{} `json:"grants"` } +// OfferedCredential defines a single entry in the credentials array of a CredentialOffer. We currently do not support 'JSON string' offers. +// Specified by https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-parameters +// and https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-vc-secured-using-data-integ +type OfferedCredential struct { + // Format specifies the credential format. + Format string `json:"format"` + // CredentialDefinition contains the 'credential_definition' for the Verifiable Credential Format flows. + CredentialDefinition *CredentialDefinition `json:"credential_definition,omitempty"` +} + +// CredentialDefinition defines the 'credential_definition' for Format VerifiableCredentialJSONLDFormat +// Specified by https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-vc-secured-using-data-integ +type CredentialDefinition struct { + Context []ssi.URI `json:"@context"` + Type []ssi.URI `json:"type"` + CredentialSubject *map[string]interface{} `json:"credentialSubject,omitempty"` // optional and currently not used +} + // CredentialOfferResponse defines the response for credential offer requests. // It is an extension to the OIDC4VCI specification to better support server-to-server issuance. type CredentialOfferResponse struct { @@ -113,7 +132,7 @@ type CredentialOfferResponse struct { // Specified by https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request. type CredentialRequest struct { Format string `json:"format"` - CredentialDefinition *map[string]interface{} `json:"credential_definition,omitempty"` + CredentialDefinition *CredentialDefinition `json:"credential_definition,omitempty"` Proof *CredentialRequestProof `json:"proof,omitempty"` } diff --git a/vcr/oidc4vci/validators.go b/vcr/oidc4vci/validators.go new file mode 100644 index 0000000000..9d6fe585b4 --- /dev/null +++ b/vcr/oidc4vci/validators.go @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package oidc4vci + +import ( + "errors" + ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/go-did/vc" +) + +// Validate the CredentialDefinition according to the VerifiableCredentialJSONLDFormat format +func (cd *CredentialDefinition) Validate(isOffer bool) error { + if cd == nil { + return errors.New("invalid credential_definition: missing") + } + if len(cd.Context) == 0 { + return errors.New("invalid credential_definition: missing @context field") + } + if len(cd.Type) == 0 { + return errors.New("invalid credential_definition: missing type field") + } + if cd.CredentialSubject != nil { + if isOffer { + return errors.New("invalid credential_definition: credentialSubject not allowed in offer") + } + // TODO: Add credentialSubject validation. + // See https://github.com/nuts-foundation/nuts-node/issues/2320 + } + return nil +} + +// ValidateDefinitionWithCredential confirms that the vc.VerifiableCredential is defined by the CredentialDefinition. +// CredentialDefinition is assumed to be valid, see ValidateCredentialDefinition. +func ValidateDefinitionWithCredential(credential vc.VerifiableCredential, definition CredentialDefinition) error { + // From spec: When the format value is ldp_vc, ..., including credential_definition object, MUST NOT be processed using JSON-LD rules. + // https://openid.bitbucket.io/connect/editors-draft/openid-4-verifiable-credential-issuance-1_0.html#name-format-identifier-2 + + // compare contexts. The credential may contain extra contexts for signatures or proofs + if len(credential.Context) < len(definition.Context) || !isSubset(credential.Context, definition.Context) { + return errors.New("credential does not match credential_definition: context mismatch") + } + + // compare types. fails when definition.Type contains duplicates + if len(credential.Type) != len(definition.Type) || !isSubset(credential.Type, definition.Type) { + return errors.New("credential does not match credential_definition: type mismatch") + } + + // TODO: Compare credentialSubject + // See https://github.com/nuts-foundation/nuts-node/issues/2320 + + return nil +} + +// isSubset is true if all elements of subset exist in set. If subset is empty it returns false. +func isSubset(set, subset []ssi.URI) bool { + if len(subset) == 0 { + return false + } + for _, el1 := range subset { + found := false + for _, el2 := range set { + if el2.String() == el1.String() { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/vcr/oidc4vci/validators_test.go b/vcr/oidc4vci/validators_test.go new file mode 100644 index 0000000000..e382ff5510 --- /dev/null +++ b/vcr/oidc4vci/validators_test.go @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package oidc4vci + +import ( + ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/go-did/vc" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_ValidateCredentialDefinition(t *testing.T) { + t.Run("definition is nil", func(t *testing.T) { + var definition *CredentialDefinition + err := definition.Validate(true) + + assert.EqualError(t, err, "invalid credential_definition: missing") + }) + t.Run("missing context", func(t *testing.T) { + definition := &CredentialDefinition{} + + err := definition.Validate(true) + + assert.EqualError(t, err, "invalid credential_definition: missing @context field") + }) + t.Run("missing type", func(t *testing.T) { + definition := &CredentialDefinition{Context: []ssi.URI{ssi.MustParseURI("http://example.com")}} + + err := definition.Validate(true) + + assert.EqualError(t, err, "invalid credential_definition: missing type field") + }) + t.Run("credentialSubject not allowed in offer", func(t *testing.T) { + definition := &CredentialDefinition{ + Context: []ssi.URI{ssi.MustParseURI("http://example.com")}, + Type: []ssi.URI{ssi.MustParseURI("SomeCredentialType")}, + CredentialSubject: new(map[string]any), + } + err := definition.Validate(true) + + assert.EqualError(t, err, "invalid credential_definition: credentialSubject not allowed in offer") + }) +} + +func Test_ValidateDefinitionWithCredential(t *testing.T) { + makeDefinition := func() CredentialDefinition { + return CredentialDefinition{ + Context: []ssi.URI{ + ssi.MustParseURI("https://www.w3.org/2018/credentials/v1"), + ssi.MustParseURI("http://example.org/credentials/V1"), + }, + Type: []ssi.URI{ + ssi.MustParseURI("VerifiableCredential"), + ssi.MustParseURI("HumanCredential"), + }, + } + } + + credential := func() vc.VerifiableCredential { + return vc.VerifiableCredential{ + Context: []ssi.URI{ + ssi.MustParseURI("https://www.w3.org/2018/credentials/v1"), + ssi.MustParseURI("http://example.org/credentials/V1"), + }, + Type: []ssi.URI{ + ssi.MustParseURI("VerifiableCredential"), + ssi.MustParseURI("HumanCredential"), + }, + } + } + + t.Run("ok", func(t *testing.T) { + assert.NoError(t, ValidateDefinitionWithCredential(credential(), makeDefinition())) + }) + t.Run("error - definition contains more contexts", func(t *testing.T) { + err := ValidateDefinitionWithCredential(vc.VerifiableCredential{}, makeDefinition()) + assert.EqualError(t, err, "credential does not match credential_definition: context mismatch") + }) + t.Run("error - different contexts", func(t *testing.T) { + definition := makeDefinition() + definition.Context = []ssi.URI{ + ssi.MustParseURI("different context"), + } + err := ValidateDefinitionWithCredential(credential(), definition) + assert.EqualError(t, err, "credential does not match credential_definition: context mismatch") + }) + t.Run("error - number of types do not match", func(t *testing.T) { + definition := makeDefinition() + definition.Type = []ssi.URI{ + ssi.MustParseURI("VerifiableCredential"), + } + err := ValidateDefinitionWithCredential(credential(), definition) + assert.EqualError(t, err, "credential does not match credential_definition: type mismatch") + }) + t.Run("error - types do not match", func(t *testing.T) { + definition := makeDefinition() + definition.Type = []ssi.URI{ + ssi.MustParseURI("VerifiableCredential"), + ssi.MustParseURI("OtherType"), + } + err := ValidateDefinitionWithCredential(credential(), definition) + assert.EqualError(t, err, "credential does not match credential_definition: type mismatch") + }) +} diff --git a/vcr/oidc4vci/wallet_client_test.go b/vcr/oidc4vci/wallet_client_test.go index fc09adf63b..5261b564e9 100644 --- a/vcr/oidc4vci/wallet_client_test.go +++ b/vcr/oidc4vci/wallet_client_test.go @@ -67,7 +67,7 @@ func Test_httpWalletClient_OfferCredential(t *testing.T) { err = client.OfferCredential(ctx, CredentialOffer{ CredentialIssuer: setup.issuerMetadata.CredentialIssuer, - Credentials: []map[string]interface{}{{"issuer": "issuer"}}, + Credentials: []OfferedCredential{}, Grants: map[string]interface{}{ "grant_type": "pre-authorized_code", }, @@ -84,7 +84,7 @@ func Test_httpWalletClient_OfferCredential(t *testing.T) { err = json.Unmarshal([]byte(credentialOfferJSON), &credentialOffer) require.NoError(t, err) require.Equal(t, setup.issuerMetadata.CredentialIssuer, credentialOffer["credential_issuer"]) - require.Equal(t, []interface{}{map[string]interface{}{"issuer": "issuer"}}, credentialOffer["credentials"]) + require.Equal(t, []interface{}{}, credentialOffer["credentials"]) require.Equal(t, map[string]interface{}{"grant_type": "pre-authorized_code"}, credentialOffer["grants"]) }) t.Run("error - invalid response from wallet", func(t *testing.T) { @@ -95,7 +95,7 @@ func Test_httpWalletClient_OfferCredential(t *testing.T) { err = client.OfferCredential(ctx, CredentialOffer{ CredentialIssuer: setup.issuerMetadata.CredentialIssuer, - Credentials: []map[string]interface{}{{"issuer": "issuer"}}, + Credentials: []OfferedCredential{}, Grants: map[string]interface{}{ "grant_type": "pre-authorized_code", }, @@ -113,7 +113,7 @@ func Test_httpWalletClient_OfferCredential(t *testing.T) { err = client.OfferCredential(ctx, CredentialOffer{ CredentialIssuer: setup.issuerMetadata.CredentialIssuer, - Credentials: []map[string]interface{}{{"issuer": "issuer"}}, + Credentials: []OfferedCredential{}, Grants: map[string]interface{}{ "grant_type": "pre-authorized_code", }, diff --git a/vcr/vcr.go b/vcr/vcr.go index 9ccd85fed7..3c7828cd7f 100644 --- a/vcr/vcr.go +++ b/vcr/vcr.go @@ -130,7 +130,7 @@ func (c *vcr) GetOpenIDHolder(ctx context.Context, id did.DID) (holder.OpenIDHan TLS: c.clientTLSConfig, HTTPSOnly: c.strictmode, } - return holder.NewOpenIDHandler(clientConfig, id, identifier, c, c.keyStore, c.keyResolver, jsonld.Reader{DocumentLoader: c.jsonldManager.DocumentLoader()}), nil + return holder.NewOpenIDHandler(clientConfig, id, identifier, c, c.keyStore, c.keyResolver), nil } func (c *vcr) resolveOpenID4VCIIdentifier(ctx context.Context, id did.DID) (string, error) {