diff --git a/component/wallet-cli/cmd/oidc4vp_cmd.go b/component/wallet-cli/cmd/oidc4vp_cmd.go index bfc6be08b..c2b8dbfb9 100644 --- a/component/wallet-cli/cmd/oidc4vp_cmd.go +++ b/component/wallet-cli/cmd/oidc4vp_cmd.go @@ -8,13 +8,15 @@ package cmd import ( "context" + "encoding/json" "fmt" - "github.com/trustbloc/vcs/component/wallet-cli/pkg/attestation" - "github.com/trustbloc/vcs/component/wallet-cli/pkg/trustregistry" "net/http" "net/url" "strings" + "github.com/trustbloc/vcs/component/wallet-cli/pkg/attestation" + "github.com/trustbloc/vcs/component/wallet-cli/pkg/trustregistry" + "github.com/henvic/httpretty" "github.com/piprate/json-gold/ld" "github.com/spf13/cobra" @@ -38,6 +40,7 @@ type oidc4vpCommandFlags struct { trustRegistryHost string attestationURL string proxyURL string + attachments string } // NewOIDC4VPCommand returns a new command for running OIDC4VP flow. @@ -87,7 +90,6 @@ func NewOIDC4VPCommand() *cobra.Command { httpClient.Transport = httpLogger.RoundTripper(httpClient.Transport) } - var walletDIDIndex int if flags.walletDIDIndex != -1 { @@ -150,6 +152,16 @@ func NewOIDC4VPCommand() *cobra.Command { oidc4vp.WithWalletDIDIndex(walletDIDIndex), } + if flags.attachments != "" { + targetAttachments := map[string]string{} + + if err = json.Unmarshal([]byte(flags.attachments), &targetAttachments); err != nil { + return fmt.Errorf("can not unmarshal attachments: %w", err) + } + + opts = append(opts, oidc4vp.WithAttachments(targetAttachments)) + } + if flags.enableLinkedDomainVerification { opts = append(opts, oidc4vp.WithLinkedDomainVerification()) } @@ -186,6 +198,8 @@ func createFlags(cmd *cobra.Command, flags *oidc4vpCommandFlags) { cmd.Flags().IntVar(&flags.walletDIDIndex, "wallet-did-index", -1, "index of wallet did, if not set the most recently created DID is used") cmd.Flags().StringVar(&flags.attestationURL, "attestation-url", "", "attestation url, i.e. https:///vcs/wallet/attestation") cmd.Flags().StringVar(&flags.trustRegistryHost, "trust-registry-host", "", "trust registry host, i.e. https:///trustregistry") + cmd.Flags().StringVar(&flags.attachments, "attachments", "", + `list of attachment. json expected. example {"some_id" : "data:image/svg;base64,YmFzZTY0Y29udGVudC1odHRwczovL2xvY2FsaG9zdC9jYXQucG5n"}`) cmd.Flags().BoolVar(&flags.enableTracing, "enable-tracing", false, "enables http tracing") cmd.Flags().StringVar(&flags.proxyURL, "proxy-url", "", "proxy url for http client") diff --git a/component/wallet-cli/pkg/oidc4vp/models.go b/component/wallet-cli/pkg/oidc4vp/models.go index 241bf0571..0d77c930c 100644 --- a/component/wallet-cli/pkg/oidc4vp/models.go +++ b/component/wallet-cli/pkg/oidc4vp/models.go @@ -49,6 +49,7 @@ type IDTokenClaims struct { Nbf int64 `json:"nbf"` Iat int64 `json:"iat"` Jti string `json:"jti"` + Attachments map[string]string `json:"_attachments"` } type VPTokenClaims struct { diff --git a/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go b/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go index 931c31011..e7ab427c3 100644 --- a/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go +++ b/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go @@ -83,6 +83,7 @@ type Flow struct { disableSchemaValidation bool perfInfo *PerfInfo useMultiVPs bool + attachments map[string]string } type provider interface { @@ -153,6 +154,7 @@ func NewFlow(p provider, opts ...Opt) (*Flow, error) { disableSchemaValidation: o.disableSchemaValidation, useMultiVPs: o.useMultiVPs, perfInfo: &PerfInfo{}, + attachments: o.attachments, }, nil } @@ -439,8 +441,11 @@ func (f *Flow) sendAuthorizationResponse( idToken, err := f.createIDToken( ctx, - requestObject.ClientID, requestObject.Nonce, requestObject.Scope, + requestObject.ClientID, + requestObject.Nonce, + requestObject.Scope, attestationRequired, + f.attachments, ) if err != nil { return fmt.Errorf("create id token: %w", err) @@ -645,8 +650,11 @@ func (f *Flow) signPresentationLDP( func (f *Flow) createIDToken( ctx context.Context, - clientID, nonce, requestObjectScope string, + clientID string, + nonce string, + requestObjectScope string, attestationRequired bool, + attachments map[string]string, ) (string, error) { scopeAdditionalClaims, err := extractCustomScopeClaims(requestObjectScope) if err != nil { @@ -663,6 +671,7 @@ func (f *Flow) createIDToken( Nbf: time.Now().Unix(), Iat: time.Now().Unix(), Jti: uuid.NewString(), + Attachments: attachments, } if attestationRequired { @@ -782,6 +791,7 @@ type options struct { disableDomainMatching bool disableSchemaValidation bool useMultiVPs bool + attachments map[string]string } type Opt func(opts *options) @@ -792,6 +802,12 @@ func WithWalletDIDIndex(idx int) Opt { } } +func WithAttachments(attachments map[string]string) func(opts *options) { + return func(opts *options) { + opts.attachments = attachments + } +} + func WithRequestURI(uri string) Opt { return func(opts *options) { opts.requestURI = uri diff --git a/pkg/restapi/v1/verifier/controller.go b/pkg/restapi/v1/verifier/controller.go index 01a9db00f..00b28e4ea 100644 --- a/pkg/restapi/v1/verifier/controller.go +++ b/pkg/restapi/v1/verifier/controller.go @@ -74,6 +74,7 @@ type IDTokenClaims struct { Nonce string `json:"nonce"` Aud string `json:"aud"` Exp int64 `json:"exp"` + Attachments map[string]string `json:"_attachments"` } type VPTokenClaims struct { @@ -629,6 +630,7 @@ func (c *Controller) verifyAuthorizationResponseTokens( CustomScopeClaims: idTokenClaims.CustomScopeClaims, VPTokens: processedVPTokens, AttestationVP: idTokenClaims.AttestationVP, + Attachments: idTokenClaims.Attachments, }, nil } @@ -684,6 +686,17 @@ func validateIDToken( Nonce: string(v.GetStringBytes("nonce")), Aud: string(v.GetStringBytes("aud")), Exp: v.GetInt64("exp"), + Attachments: map[string]string{}, + } + + if val := v.Get("_attachments"); val != nil { + o, _ := val.Object() //nolint + + if o != nil { + o.Visit(func(k []byte, v *fastjson.Value) { + idTokenClaims.Attachments[string(k)] = string(v.GetStringBytes()) + }) + } } if idTokenClaims.Exp < time.Now().Unix() { diff --git a/pkg/restapi/v1/verifier/controller_test.go b/pkg/restapi/v1/verifier/controller_test.go index 77eed2899..cc664c74b 100644 --- a/pkg/restapi/v1/verifier/controller_test.go +++ b/pkg/restapi/v1/verifier/controller_test.go @@ -28,6 +28,8 @@ import ( "github.com/trustbloc/kms-go/spi/kms" "github.com/trustbloc/vc-go/presexch" "github.com/trustbloc/vc-go/verifiable" + nooptracer "go.opentelemetry.io/otel/trace/noop" + vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" "github.com/trustbloc/vcs/pkg/event/spi" "github.com/trustbloc/vcs/pkg/internal/testutil" @@ -39,7 +41,6 @@ import ( "github.com/trustbloc/vcs/pkg/service/oidc4vp" "github.com/trustbloc/vcs/pkg/service/verifycredential" "github.com/trustbloc/vcs/pkg/service/verifypresentation" - nooptracer "go.opentelemetry.io/otel/trace/noop" ) const ( @@ -643,6 +644,75 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { require.Contains(t, authorisationResponseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") }) + t.Run("Success LDP With Attachments", func(t *testing.T) { + signedClaimsJWTResult := testutil.SignedClaimsJWT(t, + &IDTokenClaims{ + Nonce: validNonce, + Aud: validAud, + Exp: time.Now().Unix() + 1000, + Attachments: map[string]string{ + "id1": "data:image/svg;base64,YmFzZTY0Y29udGVudC1odHRwczovL2xvY2FsaG9zdC9jYXQucG5n", + }, + }, + ) + + vpSigned := testutil.SignedVPWithExistingPrivateKey(t, + &verifiable.Presentation{ + Context: []string{ + "https://www.w3.org/2018/credentials/v1", + "https://identity.foundation/presentation-exchange/submission/v1", + "https://w3id.org/security/suites/jws-2020/v1", + }, + Type: []string{ + "VerifiablePresentation", + "PresentationSubmission", + }, + }, + vcsverifiable.Ldp, + signedClaimsJWTResult.VerMethodDIDKeyID, + signedClaimsJWTResult.KeyType, + signedClaimsJWTResult.Signer, + func(ldpc *verifiable.LinkedDataProofContext) { + ldpc.Domain = validAud + ldpc.Challenge = validNonce + }) + + vpToken, err := vpSigned.MarshalJSON() + require.NoError(t, err) + + presentationSubmission, err := json.Marshal(map[string]interface{}{}) + require.NoError(t, err) + + mockEventSvc := NewMockeventService(gomock.NewController(t)) + mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).Times(0) + + c := NewController(&Config{ + OIDCVPService: svc, + EventSvc: mockEventSvc, + EventTopic: spi.VerifierEventTopic, + VDR: signedClaimsJWTResult.VDR, + DocumentLoader: testutil.DocumentLoader(t), + }) + + authorisationResponseParsed, err := c.verifyAuthorizationResponseTokens(context.TODO(), + &rawAuthorizationResponse{ + IDToken: signedClaimsJWTResult.JWT, + VPToken: []string{string(vpToken)}, + PresentationSubmission: string(presentationSubmission), + State: "txid", + }, + ) + + require.NoError(t, err) + + require.Nil(t, authorisationResponseParsed.CustomScopeClaims) + require.Contains(t, authorisationResponseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") + + require.Len(t, authorisationResponseParsed.Attachments, 1) + require.EqualValues(t, "data:image/svg;base64,YmFzZTY0Y29udGVudC1odHRwczovL2xvY2FsaG9zdC9jYXQucG5n", + authorisationResponseParsed.Attachments["id1"]) + }) + t.Run("Success JWT ID1", func(t *testing.T) { customScopeClaims := map[string]oidc4vp.Claims{ "customScope": { diff --git a/pkg/service/oidc4vp/api.go b/pkg/service/oidc4vp/api.go index f9b0b9d94..c73d3f1aa 100644 --- a/pkg/service/oidc4vp/api.go +++ b/pkg/service/oidc4vp/api.go @@ -33,6 +33,7 @@ type AuthorizationResponseParsed struct { CustomScopeClaims map[string]Claims VPTokens []*ProcessedVPToken AttestationVP string + Attachments map[string]string // Attachments from IDToken for AttachmentEvidence type } type ProcessedVPToken struct { @@ -52,10 +53,10 @@ type CredentialMetadata struct { ExpirationDate *util.TimeWrapper `json:"expirationDate,omitempty"` CustomClaims map[string]Claims `json:"customClaims,omitempty"` - Name interface{} `json:"name,omitempty"` - AwardedDate interface{} `json:"awardedDate,omitempty"` - Description interface{} `json:"description,omitempty"` - Attachments []map[string]interface{} `json:"attachments"` + Name interface{} `json:"name,omitempty"` + AwardedDate interface{} `json:"awardedDate,omitempty"` + Description interface{} `json:"description,omitempty"` + Attachments []*Attachment `json:"attachments"` } type ServiceInterface interface { @@ -152,7 +153,14 @@ type ClientMetadata struct { LogoURI string `json:"logo_uri"` } -type Attachment struct { +type attachmentData struct { Type string Claim map[string]interface{} } + +type Attachment struct { + ID string `json:"id"` + DataURI string `json:"data_uri"` + Description string `json:"description"` + Error string `json:"error,omitempty"` +} diff --git a/pkg/service/oidc4vp/attachments.go b/pkg/service/oidc4vp/attachments.go index dddf5e0a4..90ad58954 100644 --- a/pkg/service/oidc4vp/attachments.go +++ b/pkg/service/oidc4vp/attachments.go @@ -10,11 +10,15 @@ package oidc4vp import ( "context" + "crypto/sha256" + "crypto/sha512" "encoding/base64" + "encoding/hex" "errors" "fmt" "io" "net/http" + "strings" "sync" "github.com/samber/lo" @@ -23,12 +27,23 @@ import ( ) const ( - AttachmentTypeRemote = "RemoteAttachment" - AttachmentTypeEmbedded = "EmbeddedAttachment" - AttachmentDataField = "uri" + AttachmentTypeRemote = "RemoteAttachment" + AttachmentTypeEmbedded = "EmbeddedAttachment" + AttachmentEvidence = "AttachmentEvidence" + AttachmentDataField = "uri" + AttachmentIDField = "id" + AttachmentHashField = "hash" + AttachmentHashAlgoField = "hash-alg" + AttachmentErrorField = "error" + AttachmentDescriptionField = "description" ) -var knownAttachmentTypes = []string{AttachmentTypeRemote, AttachmentTypeEmbedded} // nolint:gochecknoglobals +// nolint:gochecknoglobals +var knownAttachmentTypes = []string{ + AttachmentTypeRemote, + AttachmentTypeEmbedded, + AttachmentEvidence, +} type AttachmentService struct { httpClient httpClient @@ -46,48 +61,171 @@ func NewAttachmentService( } } -func (s *AttachmentService) GetAttachments( - ctx context.Context, +func (s *AttachmentService) getAttachmentByTypes( + _ context.Context, subjects []verifiable.Subject, -) ([]map[string]interface{}, error) { - var allAttachments []*Attachment + attachmentTypes []string, +) []*attachmentData { + var allAttachments []*attachmentData for _, subject := range subjects { allAttachments = append(allAttachments, - s.findAttachments(subject.CustomFields)..., + s.findAttachments(subject.CustomFields, attachmentTypes)..., ) } + return allAttachments +} + +func (s *AttachmentService) handleEvidenceAttachment( + _ context.Context, + attachment map[string]interface{}, + idTokenAttachments map[string]string, +) error { + if len(idTokenAttachments) == 0 { + return errors.New("id token attachments are empty") + } + + idAttachment, ok := attachment[AttachmentIDField].(string) + if !ok { + return errors.New("attachment id field is required") + } + + idTokenAttachment, ok := idTokenAttachments[idAttachment] + if !ok { + return fmt.Errorf("id token attachment not found for id: %s", idAttachment) + } + + bodySegments := strings.Split(idTokenAttachment, ",") // skip data:%s;base64,%s + rawBody, err := base64.StdEncoding.DecodeString(bodySegments[len(bodySegments)-1]) + if err != nil { + return fmt.Errorf("failed to decode base64 body id token attachment: %w", err) + } + + if err = s.validateHash(attachment, rawBody); err != nil { + return fmt.Errorf("failed to validate hash for attachment id %s: %w", idAttachment, err) + } + + attachment[AttachmentDataField] = idTokenAttachment + + return nil +} + +func (s *AttachmentService) GetAttachments( + ctx context.Context, + subjects []verifiable.Subject, + idTokenAttachments map[string]string, +) ([]*Attachment, error) { + allAttachments := s.getAttachmentByTypes(ctx, subjects, knownAttachmentTypes) + if len(allAttachments) == 0 { return nil, nil } - var final []map[string]interface{} + var resultAttachments []map[string]interface{} var wg sync.WaitGroup for _, attachment := range allAttachments { cloned := maphelpers.CopyMap(attachment.Claim) attachment.Claim = cloned - final = append(final, attachment.Claim) + resultAttachments = append(resultAttachments, attachment.Claim) - if attachment.Type == AttachmentTypeRemote { + switch attachment.Type { + case AttachmentEvidence: + if err := s.handleEvidenceAttachment(ctx, cloned, idTokenAttachments); err != nil { + cloned[AttachmentErrorField] = fmt.Sprintf("failed to handle evidence attachment: %s", err) + } + case AttachmentTypeRemote: wg.Add(1) go func() { defer wg.Done() err := s.handleRemoteAttachment(ctx, cloned) if err != nil { - cloned["error"] = fmt.Sprintf("failed to handle remote attachment: %s", err) + cloned[AttachmentErrorField] = fmt.Sprintf("failed to handle remote attachment: %s", err) } }() + default: + continue } } wg.Wait() + var final []*Attachment + for _, attachment := range resultAttachments { + att := &Attachment{ + ID: fmt.Sprint(attachment[AttachmentIDField]), + } + + if v, ok := attachment[AttachmentDataField]; ok { + att.DataURI = fmt.Sprint(v) + } + if v, ok := attachment[AttachmentDescriptionField]; ok { + att.Description = fmt.Sprint(v) + } + if v, ok := attachment[AttachmentErrorField]; ok { + att.Error = fmt.Sprint(v) + } + + final = append(final, att) + } + return final, nil } +func (s *AttachmentService) validateHash( + attachment map[string]interface{}, + body []byte, +) error { + hash, ok := attachment[AttachmentHashField].(string) + if !ok { + return fmt.Errorf("attachment %s field is required", AttachmentHashField) + } + + hashAlgo, ok := attachment[AttachmentHashAlgoField].(string) + if !ok { + return fmt.Errorf("attachment %s field is required", AttachmentHashAlgoField) + } + + switch hashAlgo { + case "SHA-256": + if err := s.validateSHA256Hash(hash, body); err != nil { + return fmt.Errorf("failed to validate %v hash: %w", hashAlgo, err) + } + case "SHA-384": + if err := s.validateSHA384Hash(hash, body); err != nil { + return fmt.Errorf("failed to validate %v hash: %w", hashAlgo, err) + } + default: + return fmt.Errorf("unsupported hash algorithm: %s", hashAlgo) + } + + return nil +} + +func (s *AttachmentService) validateSHA256Hash(hash string, data []byte) error { + computedHash := sha256.Sum256(data) + computedHashHex := hex.EncodeToString(computedHash[:]) + + if computedHashHex != hash { + return errors.New("hash mismatch") + } + + return nil +} + +func (s *AttachmentService) validateSHA384Hash(hash string, data []byte) error { + computedHash := sha512.Sum384(data) + computedHashHex := hex.EncodeToString(computedHash[:]) + + if computedHashHex != hash { + return errors.New("hash mismatch") + } + + return nil +} + func (s *AttachmentService) handleRemoteAttachment( ctx context.Context, attachment map[string]interface{}, @@ -123,6 +261,10 @@ func (s *AttachmentService) handleRemoteAttachment( return fmt.Errorf("unexpected status code: %d and body %v", resp.StatusCode, string(body)) } + if err = s.validateHash(attachment, body); err != nil { + return fmt.Errorf("failed to validate hash for remote attachment: %w", err) + } + attachment[AttachmentDataField] = fmt.Sprintf("data:%s;base64,%s", resp.Header.Get("Content-Type"), base64.StdEncoding.EncodeToString(body), @@ -134,19 +276,20 @@ func (s *AttachmentService) handleRemoteAttachment( // nolint:gocognit func (s *AttachmentService) findAttachments( targetMap map[string]interface{}, -) []*Attachment { - var attachments []*Attachment + types []string, +) []*attachmentData { + var attachments []*attachmentData for k, v := range targetMap { switch valTyped := v.(type) { case []interface{}: for _, item := range valTyped { if nested, ok := item.(map[string]interface{}); ok { - attachments = append(attachments, s.findAttachments(nested)...) + attachments = append(attachments, s.findAttachments(nested, types)...) } } case map[string]interface{}: - attachments = append(attachments, s.findAttachments(valTyped)...) + attachments = append(attachments, s.findAttachments(valTyped, types)...) } if k != "type" && k != "@type" { @@ -155,8 +298,8 @@ func (s *AttachmentService) findAttachments( switch typed := v.(type) { case string: - if lo.Contains(knownAttachmentTypes, typed) { - attachments = append(attachments, &Attachment{ + if lo.Contains(types, typed) { + attachments = append(attachments, &attachmentData{ Type: typed, Claim: targetMap, }) @@ -168,8 +311,8 @@ func (s *AttachmentService) findAttachments( } for _, item := range newSlice { - if lo.Contains(knownAttachmentTypes, item) { - attachments = append(attachments, &Attachment{ + if lo.Contains(types, item) { + attachments = append(attachments, &attachmentData{ Type: item, Claim: targetMap, }) @@ -177,8 +320,8 @@ func (s *AttachmentService) findAttachments( } case []string: for _, item := range typed { - if lo.Contains(knownAttachmentTypes, item) { - attachments = append(attachments, &Attachment{ + if lo.Contains(types, item) { + attachments = append(attachments, &attachmentData{ Type: item, Claim: targetMap, }) diff --git a/pkg/service/oidc4vp/attachments_test.go b/pkg/service/oidc4vp/attachments_test.go index ba22e635e..4430b1fb9 100644 --- a/pkg/service/oidc4vp/attachments_test.go +++ b/pkg/service/oidc4vp/attachments_test.go @@ -27,6 +27,9 @@ var ( //go:embed testdata/university_degree_remote_attachment.jsonld sampleVCWithRemoteAttachment string + //go:embed testdata/university_degree_evidence_attachment.jsonld + sampleVCWithEvidenceAttachment string + //go:embed testdata/university_degree_with_attachments.jsonld sampleVCWitAttachments string ) @@ -38,7 +41,7 @@ func TestAttachment(t *testing.T) { srv := oidc4vp.NewAttachmentService(nil) - resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}) + resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, nil) assert.NoError(t, err) assert.Empty(t, resp) }) @@ -49,15 +52,13 @@ func TestAttachment(t *testing.T) { srv := oidc4vp.NewAttachmentService(nil) - resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}) + resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, nil) assert.NoError(t, err) assert.Len(t, resp, 1) attachment := resp[0] - assert.EqualValues(t, []interface{}{"EmbeddedAttachment"}, attachment["type"]) - assert.EqualValues(t, "base64content", attachment["uri"]) - assert.Nil(t, attachment["error"]) - assert.EqualValues(t, "5d41402abc4b2a76b9719d911017c592", attachment["hash"]) + assert.EqualValues(t, "base64content", attachment.DataURI) + assert.Empty(t, attachment.Error) }) t.Run("with remote attachment err", func(t *testing.T) { @@ -75,15 +76,14 @@ func TestAttachment(t *testing.T) { return nil, errors.New("connection failed") }) - resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}) + resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, nil) assert.NoError(t, err) assert.Len(t, resp, 1) attachment := resp[0] - assert.EqualValues(t, []interface{}{"RemoteAttachment"}, attachment["type"]) - assert.EqualValues(t, "https://someurl.local", attachment["uri"]) + assert.EqualValues(t, "https://someurl.local", attachment.DataURI) assert.EqualValues(t, "failed to handle remote attachment: failed to fetch url: connection failed", - attachment["error"]) + attachment.Error) }) t.Run("with remote attachment invalid status", func(t *testing.T) { @@ -106,15 +106,14 @@ func TestAttachment(t *testing.T) { }, nil }) - resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}) + resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, nil) assert.NoError(t, err) assert.Len(t, resp, 1) attachment := resp[0] - assert.EqualValues(t, []interface{}{"RemoteAttachment"}, attachment["type"]) - assert.EqualValues(t, "https://someurl.local", attachment["uri"]) + assert.EqualValues(t, "https://someurl.local", attachment.DataURI) assert.EqualValues(t, "failed to handle remote attachment: unexpected status code: 404 and body file not found", - attachment["error"]) + attachment.Error) }) t.Run("with embedded attachment as string type", func(t *testing.T) { @@ -124,15 +123,13 @@ func TestAttachment(t *testing.T) { data["credentialSubject"].(map[string]interface{})["attachment1"].(map[string]interface{})["type"] = "EmbeddedAttachment" //nolint srv := oidc4vp.NewAttachmentService(nil) - resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}) + resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, nil) assert.NoError(t, err) assert.Len(t, resp, 1) attachment := resp[0] - assert.EqualValues(t, "EmbeddedAttachment", attachment["type"]) - assert.EqualValues(t, "base64content", attachment["uri"]) - assert.Nil(t, attachment["error"]) - assert.EqualValues(t, "5d41402abc4b2a76b9719d911017c592", attachment["hash"]) + assert.EqualValues(t, "base64content", attachment.DataURI) + assert.Empty(t, attachment.Error) }) t.Run("with embedded attachment as string arr", func(t *testing.T) { @@ -142,15 +139,13 @@ func TestAttachment(t *testing.T) { data["credentialSubject"].(map[string]interface{})["attachment1"].(map[string]interface{})["type"] = []string{"EmbeddedAttachment"} //nolint srv := oidc4vp.NewAttachmentService(nil) - resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}) + resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, nil) assert.NoError(t, err) assert.Len(t, resp, 1) attachment := resp[0] - assert.EqualValues(t, []string{"EmbeddedAttachment"}, attachment["type"]) - assert.EqualValues(t, "base64content", attachment["uri"]) - assert.Nil(t, attachment["error"]) - assert.EqualValues(t, "5d41402abc4b2a76b9719d911017c592", attachment["hash"]) + assert.EqualValues(t, "base64content", attachment.DataURI) + assert.Empty(t, attachment.Error) }) t.Run("multiple attachments", func(t *testing.T) { @@ -181,7 +176,7 @@ func TestAttachment(t *testing.T) { }, nil }).Times(2) - resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}) + resp, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, nil) assert.NoError(t, err) assert.Len(t, resp, 3) @@ -189,32 +184,180 @@ func TestAttachment(t *testing.T) { assert.EqualValues(t, []string{"https://localhost/cat.png", "https://localhost/photo.png"}, urlsCalled) sort.Slice(resp, func(i, j int) bool { - return resp[i]["id"].(string) < resp[j]["id"].(string) + return resp[i].ID < resp[j].ID }) attachment := resp[1] - assert.EqualValues(t, "doc12", attachment["id"]) - assert.EqualValues(t, []interface{}{"EmbeddedAttachment"}, attachment["type"]) - assert.EqualValues(t, "base64content", attachment["uri"]) - assert.Nil(t, attachment["error"]) - assert.EqualValues(t, "5d41402abc4b2a76b9719d911017c592", attachment["hash"]) + assert.EqualValues(t, "doc12", attachment.ID) + assert.EqualValues(t, "base64content", attachment.DataURI) + assert.Empty(t, attachment.Error) attachment = resp[0] - assert.EqualValues(t, "doc1", attachment["id"]) - assert.EqualValues(t, []interface{}{"RemoteAttachment"}, attachment["type"]) + assert.EqualValues(t, "doc1", attachment.ID) assert.EqualValues(t, "data:image/svg;base64,YmFzZTY0Y29udGVudC1odHRwczovL2xvY2FsaG9zdC9jYXQucG5n", - attachment["uri"]) + attachment.DataURI) + assert.Empty(t, attachment.Error) - assert.Nil(t, attachment["error"]) - assert.EqualValues(t, "abcd", attachment["hash"]) + assert.Empty(t, attachment.Error) attachment = resp[2] - assert.EqualValues(t, "doc445", attachment["id"]) - assert.EqualValues(t, []interface{}{"RemoteAttachment"}, attachment["type"]) + assert.EqualValues(t, "doc445", attachment.ID) assert.EqualValues(t, "data:image/svg;base64,YmFzZTY0Y29udGVudC1odHRwczovL2xvY2FsaG9zdC9waG90by5wbmc=", - attachment["uri"]) + attachment.DataURI) + + assert.Empty(t, attachment.Error) + }) +} + +func TestValidateEvidences(t *testing.T) { + t.Run("no attachments", func(t *testing.T) { + var data map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleVCJsonLD), &data)) + + srv := oidc4vp.NewAttachmentService(nil) + att, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, nil) + assert.NoError(t, err) + assert.Len(t, att, 0) + }) + + t.Run("empty idToken", func(t *testing.T) { + var data map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleVCWithEvidenceAttachment), &data)) + + srv := oidc4vp.NewAttachmentService(nil) + att, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, nil) + assert.Len(t, att, 1) + assert.NoError(t, err) + + assert.Contains(t, att[0].Error, "id token attachments are empty") + }) + + t.Run("success", func(t *testing.T) { + var data map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleVCWithEvidenceAttachment), &data)) - assert.Nil(t, attachment["error"]) - assert.EqualValues(t, "xyz", attachment["hash"]) + srv := oidc4vp.NewAttachmentService(nil) + att, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, map[string]string{ + "doc1": "data:application/json;base64,aGVsbG8gd29ybGQh", + }) + + assert.Len(t, att, 1) + assert.NoError(t, err) + assert.EqualValues(t, "data:application/json;base64,aGVsbG8gd29ybGQh", att[0].DataURI) + assert.Empty(t, att[0].Error) + }) + + t.Run("success SHA-384", func(t *testing.T) { + var data map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleVCWithEvidenceAttachment), &data)) + + att := data["credentialSubject"].(map[string]interface{})["attachment1"].(map[string]interface{}) //nolint + att["hash"] = "d33d40f7010ce34aa86efd353630309ed5c3d7ffac66d988825cf699f4803ccdf3f033230612f0945332fb580d8af805" + att["hash-alg"] = "SHA-384" + + srv := oidc4vp.NewAttachmentService(nil) + attRes, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, + map[string]string{ + "doc1": "data:application/json;base64,aGVsbG8gd29ybGQh", + }) + + assert.Len(t, attRes, 1) + assert.NoError(t, err) + assert.EqualValues(t, "data:application/json;base64,aGVsbG8gd29ybGQh", attRes[0].DataURI) + assert.Empty(t, attRes[0].Error) + }) + + t.Run("attachment not found in id token", func(t *testing.T) { + var data map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleVCWithEvidenceAttachment), &data)) + + srv := oidc4vp.NewAttachmentService(nil) + att, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, + map[string]string{ + "doc2": "data:application/json;base64,aGVsbG8gd29ybGQh", + }) + + assert.NoError(t, err) + assert.Contains(t, att[0].Error, "id token attachment not found for id: doc1") + }) + + t.Run("invalid base64", func(t *testing.T) { + var data map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleVCWithEvidenceAttachment), &data)) + + srv := oidc4vp.NewAttachmentService(nil) + att, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, + map[string]string{ + "doc1": "data:application/json;base64,xxx", + }) + + assert.NoError(t, err) + assert.Contains(t, att[0].Error, "failed to decode base64 body id token attachment") + }) + + t.Run("invalid hash", func(t *testing.T) { + var data map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleVCWithEvidenceAttachment), &data)) + + srv := oidc4vp.NewAttachmentService(nil) + att, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, + map[string]string{ + "doc1": "data:application/json;base64,eHh4", + }) + + assert.NoError(t, err) + assert.Contains(t, att[0].Error, "hash: hash mismatch") + }) + + t.Run("invalid hash SHA-384", func(t *testing.T) { + var data map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleVCWithEvidenceAttachment), &data)) + + att := data["credentialSubject"].(map[string]interface{})["attachment1"].(map[string]interface{}) //nolint + att["hash-alg"] = "SHA-384" + + srv := oidc4vp.NewAttachmentService(nil) + attRes, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, + map[string]string{ + "doc1": "data:application/json;base64,aGVsbG8gd29ybGQh", + }) + + assert.NoError(t, err) + assert.Contains(t, attRes[0].Error, "hash: hash mismatch") + }) + + missingFields := []string{"id", "hash", "hash-alg"} + for _, field := range missingFields { + t.Run(fmt.Sprintf("missing %s", field), func(t *testing.T) { + var data map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleVCWithEvidenceAttachment), &data)) + + delete(data["credentialSubject"].(map[string]interface{})["attachment1"].(map[string]interface{}), field) //nolint + srv := oidc4vp.NewAttachmentService(nil) + att, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, + map[string]string{ + "doc1": "data:application/json;base64,aGVsbG8gd29ybGQh", + }) + + assert.NoError(t, err) + assert.Contains(t, att[0].Error, fmt.Sprintf("attachment %s field is required", field)) + }) + } + + t.Run("unsupported algo", func(t *testing.T) { + var data map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleVCWithEvidenceAttachment), &data)) + + att := data["credentialSubject"].(map[string]interface{})["attachment1"].(map[string]interface{}) //nolint + att["hash-alg"] = "Murmur3-NOT-SUPPORTED" + + srv := oidc4vp.NewAttachmentService(nil) + attRes, err := srv.GetAttachments(context.TODO(), []verifiable.Subject{{CustomFields: data}}, + map[string]string{ + "doc1": "data:application/json;base64,aGVsbG8gd29ybGQh", + }) + + assert.NoError(t, err) + assert.Contains(t, attRes[0].Error, "unsupported hash algorithm: Murmur3-NOT-SUPPORTED") }) } diff --git a/pkg/service/oidc4vp/claims.go b/pkg/service/oidc4vp/claims.go index f3970f977..e628cae71 100644 --- a/pkg/service/oidc4vp/claims.go +++ b/pkg/service/oidc4vp/claims.go @@ -45,7 +45,9 @@ func (tm *TxManager) ClaimsToClaimsRaw(data *ReceivedClaims) (*ReceivedClaimsRaw raw := &ReceivedClaimsRaw{ Credentials: [][]byte{}, + Attachment: data.Attachments, } + for _, cred := range data.Credentials { cl, err := json.Marshal(cred) if err != nil { @@ -88,6 +90,7 @@ func (tm *TxManager) DecryptClaims(ctx context.Context, data *ClaimData) (*Recei final := &ReceivedClaims{ Credentials: []*verifiable.Credential{}, + Attachments: raw.Attachment, } for _, v := range raw.Credentials { diff --git a/pkg/service/oidc4vp/oidc4vp_service.go b/pkg/service/oidc4vp/oidc4vp_service.go index 69f3711a9..baaf96c9a 100644 --- a/pkg/service/oidc4vp/oidc4vp_service.go +++ b/pkg/service/oidc4vp/oidc4vp_service.go @@ -101,7 +101,8 @@ type attachmentService interface { GetAttachments( ctx context.Context, subjects []verifiable.Subject, - ) ([]map[string]interface{}, error) + idTokenAttachments map[string]string, + ) ([]*Attachment, error) } type presentationVerifier interface { @@ -601,7 +602,11 @@ func (s *Service) RetrieveClaims( } if s.attachmentService != nil { - att, attErr := s.attachmentService.GetAttachments(ctx, credContents.Subject) + att, attErr := s.attachmentService.GetAttachments( + ctx, + credContents.Subject, + tx.ReceivedClaims.Attachments, + ) if attErr != nil { logger.Errorc(ctx, fmt.Sprintf("Failed to get attachments: %+v", attErr)) } @@ -712,6 +717,7 @@ func (s *Service) extractClaimData( receivedClaims := &ReceivedClaims{ CustomScopeClaims: authResponse.CustomScopeClaims, + Attachments: authResponse.Attachments, Credentials: storeCredentials, } diff --git a/pkg/service/oidc4vp/oidc4vp_service_test.go b/pkg/service/oidc4vp/oidc4vp_service_test.go index c292481ac..1cb0e5c2d 100644 --- a/pkg/service/oidc4vp/oidc4vp_service_test.go +++ b/pkg/service/oidc4vp/oidc4vp_service_test.go @@ -1067,19 +1067,23 @@ func TestService_RetrieveClaims(t *testing.T) { verifiable.WithJSONLDDocumentLoader(loader), verifiable.WithDisabledProofCheck()) - attachmentVals := []map[string]interface{}{ + attachmentVals := []*oidc4vp.Attachment{ { - "id": 123, - "uri": "base64-content", + ID: "123", + DataURI: "base64-content", }, { - "id": 456, - "uri": "base64-content2", + ID: "456", + DataURI: "base64-content2", }, } - attachmentSvc.EXPECT().GetAttachments(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, subjects []verifiable.Subject) ([]map[string]interface{}, error) { + attachmentSvc.EXPECT().GetAttachments(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func( + ctx context.Context, + subjects []verifiable.Subject, + idTokenAttachments map[string]string, + ) ([]*oidc4vp.Attachment, error) { require.Len(t, subjects, 1) require.EqualValues(t, ldvc.Contents().Subject[0], subjects[0]) diff --git a/pkg/service/oidc4vp/testdata/university_degree_evidence_attachment.jsonld b/pkg/service/oidc4vp/testdata/university_degree_evidence_attachment.jsonld new file mode 100644 index 000000000..b405c94b1 --- /dev/null +++ b/pkg/service/oidc4vp/testdata/university_degree_evidence_attachment.jsonld @@ -0,0 +1,46 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json", + "https://www.w3.org/2018/credentials/examples/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "id": "http://example.gov/credentials/3732", + "issuanceDate": "2020-03-16T22:37:26.544Z", + "expirationDate": "2030-03-16T22:37:26.544Z", + "issuer": { + "id": "did:trustblock:abc", + "name": "University" + }, + "credentialStatus": { + "id": "https://issuer-vcs.sandbox.trustbloc.dev/vc-issuer-test-2/status/1#0", + "type": "StatusList2021Entry", + "statusListIndex": "1", + "statusListCredential": "", + "statusPurpose": "2" + }, + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "degree": { + "type": "BachelorDegree", + "degree": "MIT" + }, + "name": "Jayden Doe", + "spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1", + "attachment1" : { + "id": "doc1", + "type": [ + "AttachmentEvidence" + ], + "mimeType": "image/jpeg", + "uri": "base64content", + "description": "Scanned copy of something", + "hash": "7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9", + "hash-alg": "SHA-256" + } + } +} diff --git a/pkg/service/oidc4vp/testdata/university_degree_with_attachments.jsonld b/pkg/service/oidc4vp/testdata/university_degree_with_attachments.jsonld index b67252ec8..30c06a31c 100644 --- a/pkg/service/oidc4vp/testdata/university_degree_with_attachments.jsonld +++ b/pkg/service/oidc4vp/testdata/university_degree_with_attachments.jsonld @@ -53,7 +53,8 @@ "mimeType": "image/png", "uri": "https://localhost/cat.png", "description": "Description 1", - "hash" : "abcd" + "hash": "2550cfe05f0263a1a709799e3e6728a853207c67207484aaf26661b2f5fbfc02", + "hash-alg": "SHA-256" } ] } @@ -67,7 +68,8 @@ "mimeType": "image/png", "uri": "https://localhost/photo.png", "description": "Description 1", - "hash" : "xyz" + "hash": "11431164598bcb8b83a3cb3f5eab7485dd3d4e0b14c7458e5573dc57c030af5f", + "hash-alg": "SHA-256" } } } diff --git a/pkg/service/oidc4vp/txmanager.go b/pkg/service/oidc4vp/txmanager.go index 46291d79b..332991dd6 100644 --- a/pkg/service/oidc4vp/txmanager.go +++ b/pkg/service/oidc4vp/txmanager.go @@ -40,6 +40,7 @@ type Transaction struct { } type ReceivedClaims struct { + Attachments map[string]string CustomScopeClaims map[string]Claims Credentials []*verifiable.Credential } @@ -48,6 +49,7 @@ type ReceivedClaims struct { type ReceivedClaimsRaw struct { Credentials [][]byte `json:"credentials"` CustomScopeClaims map[string][]byte `json:"customScopeClaims,omitempty"` + Attachment map[string]string `json:"attachments,omitempty"` } type ClaimData struct { diff --git a/test/bdd/features/oidc4vc_api.feature b/test/bdd/features/oidc4vc_api.feature index b98675f4c..78edc2654 100644 --- a/test/bdd/features/oidc4vc_api.feature +++ b/test/bdd/features/oidc4vc_api.feature @@ -201,9 +201,9 @@ Feature: OIDC4VC REST API | acme_issuer_no_template/v1.0 | UniversityDegreeCredential | | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | jwt | ewogICJAY29udGV4dCI6IFsKICAgICJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsCiAgICAiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiLAogICAgImh0dHBzOi8vdzNpZC5vcmcvdmMvc3RhdHVzLWxpc3QvMjAyMS92MSIKICBdLAogICJjcmVkZW50aWFsU3RhdHVzIjogewogICAgImlkIjogInVybjp1dWlkOjViYjIwNTU4LTI1Y2UtNDBiNS04MGZjLThhZjY5MWVlZGNjZSIsCiAgICAic3RhdHVzTGlzdENyZWRlbnRpYWwiOiAiaHR0cDovL3ZjLXJlc3QtZWNoby50cnVzdGJsb2MubG9jYWw6ODA3NS9pc3N1ZXIvZ3JvdXBzL2dyb3VwX2FjbWVfaXNzdWVyL2NyZWRlbnRpYWxzL3N0YXR1cy8wNmIxM2U5Mi0yN2E2LTRiNzYtOTk3Ny02ZWNlMjA3NzgzZGQiLAogICAgInN0YXR1c0xpc3RJbmRleCI6ICI2NTQ4IiwKICAgICJzdGF0dXNQdXJwb3NlIjogInJldm9jYXRpb24iLAogICAgInR5cGUiOiAiU3RhdHVzTGlzdDIwMjFFbnRyeSIKICB9LAogICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICJkZWdyZWUiOiB7CiAgICAgICJkZWdyZWUiOiAiTUlUIiwKICAgICAgInR5cGUiOiAiQmFjaGVsb3JEZWdyZWUiCiAgICB9LAogICAgImlkIjogImRpZDppb246RWlCeVFBeFhtT0FFTTdUbEVVRzl1NlJyMnJzNVFDTHh1T2ZWbE5ONXpUM1JtUTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKaFpHUXRjSFZpYkdsakxXdGxlWE1pTENKd2RXSnNhV05MWlhseklqcGJleUpwWkNJNklsOXVWVGx5YmtGcGNIZzNNRmhYU0RSS2RVcDNUMVZwV2paNFNVWk5ZVEZuUm1oS1VWVXdWSFoyTTFFaUxDSndkV0pzYVdOTFpYbEtkMnNpT25zaVkzSjJJam9pVUMwek9EUWlMQ0pyYVdRaU9pSmZibFU1Y201QmFYQjROekJZVjBnMFNuVktkMDlWYVZvMmVFbEdUV0V4WjBab1NsRlZNRlIyZGpOUklpd2lhM1I1SWpvaVJVTWlMQ0o0SWpvaU5sYzNkSEZhTVhabFRUZFpTRTU1ZEROME9YaE1PVGRNVDBoelp6a3libTV3WDJSc2FXTlFjVlJsY2pSdFNYaFplVFZ4TTA1MmVVa3djekkxV2tsRk9DSXNJbmtpT2lKRGExUlhaaTFZVlZwNkxWZDJOekl0TlhCdk9XZHBjblo2U25kNE1uRlRRMGt5Wm1GNWJIZFhjbXR0YW5wWE16RkVjV0ZIY2paRldrUXRkVmxEVTJkT0luMHNJbkIxY25CdmMyVnpJanBiSW1GMWRHaGxiblJwWTJGMGFXOXVJaXdpWVhOelpYSjBhVzl1VFdWMGFHOWtJbDBzSW5SNWNHVWlPaUpLYzI5dVYyVmlTMlY1TWpBeU1DSjlYWDFkTENKMWNHUmhkR1ZEYjIxdGFYUnRaVzUwSWpvaVJXbERNbWh0YzJSRk5IRk9kMjFUUTFCbU1qRjFNRXg2UWtodmMzQldVRkZzT1ZKdFRHczNORloxWnpWcmR5SjlMQ0p6ZFdabWFYaEVZWFJoSWpwN0ltUmxiSFJoU0dGemFDSTZJa1ZwUVMxWk9YVjBUVTVoYVVoVlIxcHROVEJtUVZKamNWTmFjVWRCTXpkNlFsWlVOVVk0UzBsdFh6TlRka0VpTENKeVpXTnZkbVZ5ZVVOdmJXMXBkRzFsYm5RaU9pSkZhVUpSYlVOaWRsOVZRelZSUmtOMk9VTndiMUpQYVZNNWJIWkNkbmR1WTJsWkxWOTRjbEU0U1RKdlIwSm5JbjBzSW5SNWNHVWlPaUpqY21WaGRHVWlmUSIsCiAgICAibmFtZSI6ICJKYXlkZW4gRG9lIiwKICAgICJzcG91c2UiOiAiZGlkOmV4YW1wbGU6YzI3NmUxMmVjMjFlYmZlYjFmNzEyZWJjNmYxIgogIH0sCiAgImV4cGlyYXRpb25EYXRlIjogIjIwMjUtMDMtMThUMjI6MDY6MzEuMzQyNTAxMDZaIiwKICAiaWQiOiAidXJuOnV1aWQ6N2NiYjliYTItMzgwMS00Nzg2LWI3YzEtYWQyZDQyOTI0M2E4IiwKICAiaXNzdWFuY2VEYXRlIjogIjIwMjQtMDMtMThUMjI6MDY6MzEuMzc1NDM0MTMxWiIsCiAgImlzc3VlciI6IHsKICAgICJpZCI6ICJkaWQ6aW9uOkVpQW9hSEpZRjJRNms5eml5aDRQRHJ1c0ladE5VS20xLWFGckF2clBEQ0VNU1E6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSmhaR1F0Y0hWaWJHbGpMV3RsZVhNaUxDSndkV0pzYVdOTFpYbHpJanBiZXlKcFpDSTZJbU0xTkdRME1qZzJMV0l5TmpNdE5ESTNNQzFoTURNeExUVXlPVEppWXpSak5qUmpaQ0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpsSWpvaVFWRkJRaUlzSW10cFpDSTZJbU0xTkdRME1qZzJMV0l5TmpNdE5ESTNNQzFoTURNeExUVXlPVEppWXpSak5qUmpaQ0lzSW10MGVTSTZJbEpUUVNJc0ltNGlPaUp1V1RCaU5tRkdURTVWYW1RM2VFRnBiVnA0ZFVnd1prNVNUVWN6WldOSFdHOVpTM1JSV0RSMVMxRmpjMWxaVTI1R2FFRkRlRGhtYW5Cbk5qSkplVnBDZFZSUVlUWnVaM04yWDA1UlFraFVXVWxNUmpKUldWVlFhVFJRVkVzdGVVRTNUbVJQU2xad05VRkZWelZ5YzNsc1pUZ3dTeTFQVUdsVVVXMUtSMng1VWpoSlVrWklOazl0TTNNMllYTnRXVmw1WmtrdExXdFJaVGhJYTBkMGJYYzNUR2xpVldWWlduZDNSemxJU1d4NlNEUlFObWhJVDNOVVpsaGhSRmwyYkRVM1dXcDFibWd5Y1ZVeVlXbFhSRms1UVdsVVVrSnBiVXhDYUVsdVpuWTBOVTVZZVRsMWQzSkdPWE5aYkZodWJHZDViVlZUTVZGV2NUYzVWUzFsYmt0bVJYTXpSMnMyUlVSaVYzSkhiWHBIY0hSUFpHcHpkRWt3T1dWNGFWUklPRXRyZG5oT1JXaFFla3hrZW5rd1VGTmxSV1o0WDJsVFdXeE1aRmxKUmtWVU4yb3hNVTV2TFRGeU1rVjZRa05wVVRWdWRGSXhNMUVpZlN3aWNIVnljRzl6WlhNaU9sc2lZWFYwYUdWdWRHbGpZWFJwYjI0aUxDSmhjM05sY25ScGIyNU5aWFJvYjJRaVhTd2lkSGx3WlNJNklrcHpiMjVYWldKTFpYa3lNREl3SW4xZGZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVORVJsRlFabmhTUm5Oc2NrRkliVm80YVhOUWJFNDRRelpLVW1SemVuUTBabkkwYURsSU5HUm1NVlpuSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEJTRE14TkZkcVJUTkJhVFJoTFhNeVNtOUhjWFpqUzFWcFRFUmZNVEpWWjNWeFkwWTFiVWR6Um5aMFp5SXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFTMXBka1pxVFRaTGRteDJZVXRVT0dkQ2EzQkphelpMWmt4dk9VbDVNR28wWVdobWFpMXJUMGwyV2xFaWZTd2lkSGx3WlNJNkltTnlaV0YwWlNKOSIsCiAgICAibmFtZSI6ICJBY21lIElzc3VlciIKICB9LAogICJ0eXBlIjogWwogICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCIKICBdCn0= | @oidc4vc_rest_pre_auth_flow_compose_with_attachment - Scenario Outline: OIDC credential issuance and verification Pre Auth flow + Scenario Outline: OIDC credential issuance and verification Pre Auth flow (attachment evidence) Given Profile "" issuer has been authorized with username "profile-user-issuer-1" and password "profile-user-issuer-1-pwd" - And User holds credential "" with templateID "" + And User holds credential "" with templateID "" And Profile "" verifier has been authorized with username "profile-user-verifier-1" and password "profile-user-verifier-1-pwd" And proofType is "" And initiateIssuanceVersion is "2" @@ -219,9 +219,30 @@ Feature: OIDC4VC REST API And Verifier with profile "" requests deleted interactions claims Examples: - | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | proofType | credentialEncoded | - | bank_issuer/v1.0 | UniversityDegreeCredential | | v_myprofile_jwt_no_strict/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | jwt | ewogICJAY29udGV4dCI6IFsKICAgICJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsCiAgICAiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiLAogICAgImh0dHBzOi8vdzNpZC5vcmcvdmMvc3RhdHVzLWxpc3QvMjAyMS92MSIKICBdLAogICJjcmVkZW50aWFsU3RhdHVzIjogewogICAgImlkIjogInVybjp1dWlkOjViYjIwNTU4LTI1Y2UtNDBiNS04MGZjLThhZjY5MWVlZGNjZSIsCiAgICAic3RhdHVzTGlzdENyZWRlbnRpYWwiOiAiaHR0cDovL3ZjLXJlc3QtZWNoby50cnVzdGJsb2MubG9jYWw6ODA3NS9pc3N1ZXIvZ3JvdXBzL2dyb3VwX2FjbWVfaXNzdWVyL2NyZWRlbnRpYWxzL3N0YXR1cy8wNmIxM2U5Mi0yN2E2LTRiNzYtOTk3Ny02ZWNlMjA3NzgzZGQiLAogICAgInN0YXR1c0xpc3RJbmRleCI6ICI2NTQ4IiwKICAgICJzdGF0dXNQdXJwb3NlIjogInJldm9jYXRpb24iLAogICAgInR5cGUiOiAiU3RhdHVzTGlzdDIwMjFFbnRyeSIKICB9LAogICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICJkZWdyZWUiOiB7CiAgICAgICJkZWdyZWUiOiAiTUlUIiwKICAgICAgInR5cGUiOiAiQmFjaGVsb3JEZWdyZWUiCiAgICB9LAogICAgInBob3RvIjogewogICAgICAiaWQiOiAiZG9jNDQ1IiwKICAgICAgInR5cGUiOiBbCiAgICAgICAgIkVtYmVkZGVkQXR0YWNobWVudCIKICAgICAgXSwKICAgICAgIm1pbWVUeXBlIjogImltYWdlL3BuZyIsCiAgICAgICJ1cmkiOiAiZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dva0pnIiwKICAgICAgImRlc2NyaXB0aW9uIjogImIiCiAgICB9LAogICAgInJlbW90ZSI6IHsKICAgICAgImlkIjogInJlbW90ZV9kb2MiLAogICAgICAidHlwZSI6IFsKICAgICAgICAiUmVtb3RlQXR0YWNobWVudCIKICAgICAgXSwKICAgICAgIm1pbWVUeXBlIjogImltYWdlL3N2ZyIsCiAgICAgICJ1cmkiOiAiaHR0cHM6Ly93d3cudzMub3JnL2Fzc2V0cy9sb2dvcy93M2MvdzNjLW5vLWJhcnMuc3ZnIiwKICAgICAgImRlc2NyaXB0aW9uIjogImEiCiAgICB9LAogICAgImlkIjogImRpZDppb246RWlCeVFBeFhtT0FFTTdUbEVVRzl1NlJyMnJzNVFDTHh1T2ZWbE5ONXpUM1JtUTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKaFpHUXRjSFZpYkdsakxXdGxlWE1pTENKd2RXSnNhV05MWlhseklqcGJleUpwWkNJNklsOXVWVGx5YmtGcGNIZzNNRmhYU0RSS2RVcDNUMVZwV2paNFNVWk5ZVEZuUm1oS1VWVXdWSFoyTTFFaUxDSndkV0pzYVdOTFpYbEtkMnNpT25zaVkzSjJJam9pVUMwek9EUWlMQ0pyYVdRaU9pSmZibFU1Y201QmFYQjROekJZVjBnMFNuVktkMDlWYVZvMmVFbEdUV0V4WjBab1NsRlZNRlIyZGpOUklpd2lhM1I1SWpvaVJVTWlMQ0o0SWpvaU5sYzNkSEZhTVhabFRUZFpTRTU1ZEROME9YaE1PVGRNVDBoelp6a3libTV3WDJSc2FXTlFjVlJsY2pSdFNYaFplVFZ4TTA1MmVVa3djekkxV2tsRk9DSXNJbmtpT2lKRGExUlhaaTFZVlZwNkxWZDJOekl0TlhCdk9XZHBjblo2U25kNE1uRlRRMGt5Wm1GNWJIZFhjbXR0YW5wWE16RkVjV0ZIY2paRldrUXRkVmxEVTJkT0luMHNJbkIxY25CdmMyVnpJanBiSW1GMWRHaGxiblJwWTJGMGFXOXVJaXdpWVhOelpYSjBhVzl1VFdWMGFHOWtJbDBzSW5SNWNHVWlPaUpLYzI5dVYyVmlTMlY1TWpBeU1DSjlYWDFkTENKMWNHUmhkR1ZEYjIxdGFYUnRaVzUwSWpvaVJXbERNbWh0YzJSRk5IRk9kMjFUUTFCbU1qRjFNRXg2UWtodmMzQldVRkZzT1ZKdFRHczNORloxWnpWcmR5SjlMQ0p6ZFdabWFYaEVZWFJoSWpwN0ltUmxiSFJoU0dGemFDSTZJa1ZwUVMxWk9YVjBUVTVoYVVoVlIxcHROVEJtUVZKamNWTmFjVWRCTXpkNlFsWlVOVVk0UzBsdFh6TlRka0VpTENKeVpXTnZkbVZ5ZVVOdmJXMXBkRzFsYm5RaU9pSkZhVUpSYlVOaWRsOVZRelZSUmtOMk9VTndiMUpQYVZNNWJIWkNkbmR1WTJsWkxWOTRjbEU0U1RKdlIwSm5JbjBzSW5SNWNHVWlPaUpqY21WaGRHVWlmUSIsCiAgICAibmFtZSI6ICJKYXlkZW4gRG9lIiwKICAgICJzcG91c2UiOiAiZGlkOmV4YW1wbGU6YzI3NmUxMmVjMjFlYmZlYjFmNzEyZWJjNmYxIgogIH0sCiAgImV4cGlyYXRpb25EYXRlIjogIjIwMjUtMDMtMThUMjI6MDY6MzEuMzQyNTAxMDZaIiwKICAiaWQiOiAidXJuOnV1aWQ6N2NiYjliYTItMzgwMS00Nzg2LWI3YzEtYWQyZDQyOTI0M2E4IiwKICAiaXNzdWFuY2VEYXRlIjogIjIwMjQtMDMtMThUMjI6MDY6MzEuMzc1NDM0MTMxWiIsCiAgImlzc3VlciI6IHsKICAgICJpZCI6ICJkaWQ6aW9uOkVpQW9hSEpZRjJRNms5eml5aDRQRHJ1c0ladE5VS20xLWFGckF2clBEQ0VNU1E6ZXlKa1pXeDBZU0k2ZXlKd1lYUmphR1Z6SWpwYmV5SmhZM1JwYjI0aU9pSmhaR1F0Y0hWaWJHbGpMV3RsZVhNaUxDSndkV0pzYVdOTFpYbHpJanBiZXlKcFpDSTZJbU0xTkdRME1qZzJMV0l5TmpNdE5ESTNNQzFoTURNeExUVXlPVEppWXpSak5qUmpaQ0lzSW5CMVlteHBZMHRsZVVwM2F5STZleUpsSWpvaVFWRkJRaUlzSW10cFpDSTZJbU0xTkdRME1qZzJMV0l5TmpNdE5ESTNNQzFoTURNeExUVXlPVEppWXpSak5qUmpaQ0lzSW10MGVTSTZJbEpUUVNJc0ltNGlPaUp1V1RCaU5tRkdURTVWYW1RM2VFRnBiVnA0ZFVnd1prNVNUVWN6WldOSFdHOVpTM1JSV0RSMVMxRmpjMWxaVTI1R2FFRkRlRGhtYW5Cbk5qSkplVnBDZFZSUVlUWnVaM04yWDA1UlFraFVXVWxNUmpKUldWVlFhVFJRVkVzdGVVRTNUbVJQU2xad05VRkZWelZ5YzNsc1pUZ3dTeTFQVUdsVVVXMUtSMng1VWpoSlVrWklOazl0TTNNMllYTnRXVmw1WmtrdExXdFJaVGhJYTBkMGJYYzNUR2xpVldWWlduZDNSemxJU1d4NlNEUlFObWhJVDNOVVpsaGhSRmwyYkRVM1dXcDFibWd5Y1ZVeVlXbFhSRms1UVdsVVVrSnBiVXhDYUVsdVpuWTBOVTVZZVRsMWQzSkdPWE5aYkZodWJHZDViVlZUTVZGV2NUYzVWUzFsYmt0bVJYTXpSMnMyUlVSaVYzSkhiWHBIY0hSUFpHcHpkRWt3T1dWNGFWUklPRXRyZG5oT1JXaFFla3hrZW5rd1VGTmxSV1o0WDJsVFdXeE1aRmxKUmtWVU4yb3hNVTV2TFRGeU1rVjZRa05wVVRWdWRGSXhNMUVpZlN3aWNIVnljRzl6WlhNaU9sc2lZWFYwYUdWdWRHbGpZWFJwYjI0aUxDSmhjM05sY25ScGIyNU5aWFJvYjJRaVhTd2lkSGx3WlNJNklrcHpiMjVYWldKTFpYa3lNREl3SW4xZGZWMHNJblZ3WkdGMFpVTnZiVzFwZEcxbGJuUWlPaUpGYVVORVJsRlFabmhTUm5Oc2NrRkliVm80YVhOUWJFNDRRelpLVW1SemVuUTBabkkwYURsSU5HUm1NVlpuSW4wc0luTjFabVpwZUVSaGRHRWlPbnNpWkdWc2RHRklZWE5vSWpvaVJXbEJTRE14TkZkcVJUTkJhVFJoTFhNeVNtOUhjWFpqUzFWcFRFUmZNVEpWWjNWeFkwWTFiVWR6Um5aMFp5SXNJbkpsWTI5MlpYSjVRMjl0YldsMGJXVnVkQ0k2SWtWcFFTMXBka1pxVFRaTGRteDJZVXRVT0dkQ2EzQkphelpMWmt4dk9VbDVNR28wWVdobWFpMXJUMGwyV2xFaWZTd2lkSGx3WlNJNkltTnlaV0YwWlNKOSIsCiAgICAibmFtZSI6ICJBY21lIElzc3VlciIKICB9LAogICJ0eXBlIjogWwogICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCIKICBdCn0= | + | issuerProfile | credentialType | verifierProfile | presentationDefinitionID | fields | proofType | credentialEncoded | + | bank_issuer/v1.0 | UniversityDegreeCredential | v_myprofile_jwt_no_strict/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | jwt | ewogICJAY29udGV4dCI6IFsKICAgICJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsCiAgICAiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiLAogICAgImh0dHBzOi8vdzNpZC5vcmcvdmMvc3RhdHVzLWxpc3QvMjAyMS92MSIKICBdLAogICJjcmVkZW50aWFsU3RhdHVzIjogewogICAgImlkIjogInVybjp1dWlkOjViYjIwNTU4LTI1Y2UtNDBiNS04MGZjLThhZjY5MWVlZGNjZSIsCiAgICAic3RhdHVzTGlzdENyZWRlbnRpYWwiOiAiaHR0cDovL3ZjLXJlc3QtZWNoby50cnVzdGJsb2MubG9jYWw6ODA3NS9pc3N1ZXIvZ3JvdXBzL2dyb3VwX2FjbWVfaXNzdWVyL2NyZWRlbnRpYWxzL3N0YXR1cy8wNmIxM2U5Mi0yN2E2LTRiNzYtOTk3Ny02ZWNlMjA3NzgzZGQiLAogICAgInN0YXR1c0xpc3RJbmRleCI6ICI2NTQ4IiwKICAgICJzdGF0dXNQdXJwb3NlIjogInJldm9jYXRpb24iLAogICAgInR5cGUiOiAiU3RhdHVzTGlzdDIwMjFFbnRyeSIKICB9LAogICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICJkZWdyZWUiOiB7CiAgICAgICJkZWdyZWUiOiAiTUlUIiwKICAgICAgInR5cGUiOiAiQmFjaGVsb3JEZWdyZWUiCiAgICB9LAogICAgInBob3RvIjogewogICAgICAiaWQiOiAiZG9jNDQ1IiwKICAgICAgInR5cGUiOiBbCiAgICAgICAgIkVtYmVkZGVkQXR0YWNobWVudCIKICAgICAgXSwKICAgICAgIm1pbWVUeXBlIjogImltYWdlL3BuZyIsCiAgICAgICJ1cmkiOiAiZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dva0pnIiwKICAgICAgImRlc2NyaXB0aW9uIjogImIiCiAgICB9LAogICAgInJlbW90ZSI6IHsKICAgICAgImlkIjogInJlbW90ZV9kb2MiLAogICAgICAidHlwZSI6IFsKICAgICAgICAiUmVtb3RlQXR0YWNobWVudCIKICAgICAgXSwKICAgICAgIm1pbWVUeXBlIjogImltYWdlL3N2ZyIsCiAgICAgICJ1cmkiOiAiaHR0cHM6Ly93d3cudzMub3JnL2Fzc2V0cy9sb2dvcy93M2MvdzNjLW5vLWJhcnMuc3ZnIiwKICAgICAgImRlc2NyaXB0aW9uIjogImEiLAogICAgICAiaGFzaC1hbGciIDogIlNIQS0yNTYiLAogICAgICAiaGFzaCIgOiAiNzk2NjYzYzM1YTEzZWJkZTAwZDk5ZDQxNDliYjBiMTEyNGUzOTFmZmY5OWU4ZjU1ZTg5MDhkNGM4YjE1ZmI2ZCIKICAgIH0sCiAgICAiaWQiOiAiZGlkOmlvbjpFaUJ5UUF4WG1PQUVNN1RsRVVHOXU2UnIycnM1UUNMeHVPZlZsTk41elQzUm1ROmV5SmtaV3gwWVNJNmV5SndZWFJqYUdWeklqcGJleUpoWTNScGIyNGlPaUpoWkdRdGNIVmliR2xqTFd0bGVYTWlMQ0p3ZFdKc2FXTkxaWGx6SWpwYmV5SnBaQ0k2SWw5dVZUbHlia0ZwY0hnM01GaFhTRFJLZFVwM1QxVnBXalo0U1VaTllURm5SbWhLVVZVd1ZIWjJNMUVpTENKd2RXSnNhV05MWlhsS2Qyc2lPbnNpWTNKMklqb2lVQzB6T0RRaUxDSnJhV1FpT2lKZmJsVTVjbTVCYVhCNE56QllWMGcwU25WS2QwOVZhVm8yZUVsR1RXRXhaMFpvU2xGVk1GUjJkak5SSWl3aWEzUjVJam9pUlVNaUxDSjRJam9pTmxjM2RIRmFNWFpsVFRkWlNFNTVkRE4wT1hoTU9UZE1UMGh6WnpreWJtNXdYMlJzYVdOUWNWUmxjalJ0U1hoWmVUVnhNMDUyZVVrd2N6STFXa2xGT0NJc0lua2lPaUpEYTFSWFppMVlWVnA2TFZkMk56SXROWEJ2T1dkcGNuWjZTbmQ0TW5GVFEwa3labUY1YkhkWGNtdHRhbnBYTXpGRWNXRkhjalpGV2tRdGRWbERVMmRPSW4wc0luQjFjbkJ2YzJWeklqcGJJbUYxZEdobGJuUnBZMkYwYVc5dUlpd2lZWE56WlhKMGFXOXVUV1YwYUc5a0lsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhYMWRMQ0oxY0dSaGRHVkRiMjF0YVhSdFpXNTBJam9pUldsRE1taHRjMlJGTkhGT2QyMVRRMUJtTWpGMU1FeDZRa2h2YzNCV1VGRnNPVkp0VEdzM05GWjFaelZyZHlKOUxDSnpkV1ptYVhoRVlYUmhJanA3SW1SbGJIUmhTR0Z6YUNJNklrVnBRUzFaT1hWMFRVNWhhVWhWUjFwdE5UQm1RVkpqY1ZOYWNVZEJNemQ2UWxaVU5VWTRTMGx0WHpOVGRrRWlMQ0p5WldOdmRtVnllVU52YlcxcGRHMWxiblFpT2lKRmFVSlJiVU5pZGw5VlF6VlJSa04yT1VOd2IxSlBhVk01YkhaQ2RuZHVZMmxaTFY5NGNsRTRTVEp2UjBKbkluMHNJblI1Y0dVaU9pSmpjbVZoZEdVaWZRIiwKICAgICJuYW1lIjogIkpheWRlbiBEb2UiLAogICAgInNwb3VzZSI6ICJkaWQ6ZXhhbXBsZTpjMjc2ZTEyZWMyMWViZmViMWY3MTJlYmM2ZjEiCiAgfSwKICAiZXhwaXJhdGlvbkRhdGUiOiAiMjAyNS0wMy0xOFQyMjowNjozMS4zNDI1MDEwNloiLAogICJpZCI6ICJ1cm46dXVpZDo3Y2JiOWJhMi0zODAxLTQ3ODYtYjdjMS1hZDJkNDI5MjQzYTgiLAogICJpc3N1YW5jZURhdGUiOiAiMjAyNC0wMy0xOFQyMjowNjozMS4zNzU0MzQxMzFaIiwKICAiaXNzdWVyIjogewogICAgImlkIjogImRpZDppb246RWlBb2FISllGMlE2azl6aXloNFBEcnVzSVp0TlVLbTEtYUZyQXZyUERDRU1TUTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKaFpHUXRjSFZpYkdsakxXdGxlWE1pTENKd2RXSnNhV05MWlhseklqcGJleUpwWkNJNkltTTFOR1EwTWpnMkxXSXlOak10TkRJM01DMWhNRE14TFRVeU9USmlZelJqTmpSalpDSXNJbkIxWW14cFkwdGxlVXAzYXlJNmV5SmxJam9pUVZGQlFpSXNJbXRwWkNJNkltTTFOR1EwTWpnMkxXSXlOak10TkRJM01DMWhNRE14TFRVeU9USmlZelJqTmpSalpDSXNJbXQwZVNJNklsSlRRU0lzSW00aU9pSnVXVEJpTm1GR1RFNVZhbVEzZUVGcGJWcDRkVWd3Wms1U1RVY3paV05IV0c5WlMzUlJXRFIxUzFGamMxbFpVMjVHYUVGRGVEaG1hbkJuTmpKSmVWcENkVlJRWVRadVozTjJYMDVSUWtoVVdVbE1SakpSV1ZWUWFUUlFWRXN0ZVVFM1RtUlBTbFp3TlVGRlZ6VnljM2xzWlRnd1N5MVBVR2xVVVcxS1IyeDVVamhKVWtaSU5rOXRNM00yWVhOdFdWbDVaa2t0TFd0UlpUaElhMGQwYlhjM1RHbGlWV1ZaV25kM1J6bElTV3g2U0RSUU5taElUM05VWmxoaFJGbDJiRFUzV1dwMWJtZ3ljVlV5WVdsWFJGazVRV2xVVWtKcGJVeENhRWx1Wm5ZME5VNVllVGwxZDNKR09YTlpiRmh1YkdkNWJWVlRNVkZXY1RjNVZTMWxia3RtUlhNelIyczJSVVJpVjNKSGJYcEhjSFJQWkdwemRFa3dPV1Y0YVZSSU9FdHJkbmhPUldoUWVreGtlbmt3VUZObFJXWjRYMmxUV1d4TVpGbEpSa1ZVTjJveE1VNXZMVEZ5TWtWNlFrTnBVVFZ1ZEZJeE0xRWlmU3dpY0hWeWNHOXpaWE1pT2xzaVlYVjBhR1Z1ZEdsallYUnBiMjRpTENKaGMzTmxjblJwYjI1TlpYUm9iMlFpWFN3aWRIbHdaU0k2SWtwemIyNVhaV0pMWlhreU1ESXdJbjFkZlYwc0luVndaR0YwWlVOdmJXMXBkRzFsYm5RaU9pSkZhVU5FUmxGUVpuaFNSbk5zY2tGSWJWbzRhWE5RYkU0NFF6WktVbVJ6ZW5RMFpuSTBhRGxJTkdSbU1WWm5JbjBzSW5OMVptWnBlRVJoZEdFaU9uc2laR1ZzZEdGSVlYTm9Jam9pUldsQlNETXhORmRxUlROQmFUUmhMWE15U205SGNYWmpTMVZwVEVSZk1USlZaM1Z4WTBZMWJVZHpSblowWnlJc0luSmxZMjkyWlhKNVEyOXRiV2wwYldWdWRDSTZJa1ZwUVMxcGRrWnFUVFpMZG14MllVdFVPR2RDYTNCSmF6Wkxaa3h2T1VsNU1HbzBZV2htYWkxclQwbDJXbEVpZlN3aWRIbHdaU0k2SW1OeVpXRjBaU0o5IiwKICAgICJuYW1lIjogIkFjbWUgSXNzdWVyIgogIH0sCiAgInR5cGUiOiBbCiAgICAiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLAogICAgIlVuaXZlcnNpdHlEZWdyZWVDcmVkZW50aWFsIgogIF0KfQ== | + + @oidc4vc_rest_pre_auth_flow_compose_with_attachment_evidence + Scenario Outline: OIDC credential issuance and verification Pre Auth flow + Given Profile "" issuer has been authorized with username "profile-user-issuer-1" and password "profile-user-issuer-1-pwd" + And User holds credential "" with templateID "" + And Profile "" verifier has been authorized with username "profile-user-verifier-1" and password "profile-user-verifier-1-pwd" + And proofType is "" + And initiateIssuanceVersion is "2" + And credentialCompose is active with "" + When User interacts with Wallet to initiate credential issuance using pre authorization code flow + Then "1" credentials are issued + Then wallet add attachments to vp flow with data '' + And expected attachment for vp flow is "data:text/plain;base64,aGVsbG8=" + Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile with presentation definition ID "" and fields "" + And Verifier with profile "" retrieves interactions claims + Then we wait 2 seconds + And Verifier with profile "" requests deleted interactions claims + + Examples: + | issuerProfile | credentialType | attachmentData | verifierProfile | presentationDefinitionID | fields | proofType | credentialEncoded | + | bank_issuer/v1.0 | UniversityDegreeCredential | { "evidence_doc" : "data:text/plain;base64,aGVsbG8=" } | v_myprofile_jwt_no_strict/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | jwt | ewogICJAY29udGV4dCI6IFsKICAgICJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsCiAgICAiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiLAogICAgImh0dHBzOi8vdzNpZC5vcmcvdmMvc3RhdHVzLWxpc3QvMjAyMS92MSIKICBdLAogICJjcmVkZW50aWFsU3RhdHVzIjogewogICAgImlkIjogInVybjp1dWlkOjViYjIwNTU4LTI1Y2UtNDBiNS04MGZjLThhZjY5MWVlZGNjZSIsCiAgICAic3RhdHVzTGlzdENyZWRlbnRpYWwiOiAiaHR0cDovL3ZjLXJlc3QtZWNoby50cnVzdGJsb2MubG9jYWw6ODA3NS9pc3N1ZXIvZ3JvdXBzL2dyb3VwX2FjbWVfaXNzdWVyL2NyZWRlbnRpYWxzL3N0YXR1cy8wNmIxM2U5Mi0yN2E2LTRiNzYtOTk3Ny02ZWNlMjA3NzgzZGQiLAogICAgInN0YXR1c0xpc3RJbmRleCI6ICI2NTQ4IiwKICAgICJzdGF0dXNQdXJwb3NlIjogInJldm9jYXRpb24iLAogICAgInR5cGUiOiAiU3RhdHVzTGlzdDIwMjFFbnRyeSIKICB9LAogICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICJkZWdyZWUiOiB7CiAgICAgICJkZWdyZWUiOiAiTUlUIiwKICAgICAgInR5cGUiOiAiQmFjaGVsb3JEZWdyZWUiCiAgICB9LAogICAgImV2aWRlbmNlIjogewogICAgICAiaWQiOiAiZXZpZGVuY2VfZG9jIiwKICAgICAgInR5cGUiOiBbCiAgICAgICAgIkF0dGFjaG1lbnRFdmlkZW5jZSIKICAgICAgXSwKICAgICAgImhhc2giIDogIjJjZjI0ZGJhNWZiMGEzMGUyNmU4M2IyYWM1YjllMjllMWIxNjFlNWMxZmE3NDI1ZTczMDQzMzYyOTM4Yjk4MjQiLAogICAgICAiaGFzaC1hbGciIDogIlNIQS0yNTYiLAogICAgICAibWltZVR5cGUiOiAidGV4dC9wbGFpbiIKICAgIH0sCiAgICAiaWQiOiAiZGlkOmlvbjpFaUJ5UUF4WG1PQUVNN1RsRVVHOXU2UnIycnM1UUNMeHVPZlZsTk41elQzUm1ROmV5SmtaV3gwWVNJNmV5SndZWFJqYUdWeklqcGJleUpoWTNScGIyNGlPaUpoWkdRdGNIVmliR2xqTFd0bGVYTWlMQ0p3ZFdKc2FXTkxaWGx6SWpwYmV5SnBaQ0k2SWw5dVZUbHlia0ZwY0hnM01GaFhTRFJLZFVwM1QxVnBXalo0U1VaTllURm5SbWhLVVZVd1ZIWjJNMUVpTENKd2RXSnNhV05MWlhsS2Qyc2lPbnNpWTNKMklqb2lVQzB6T0RRaUxDSnJhV1FpT2lKZmJsVTVjbTVCYVhCNE56QllWMGcwU25WS2QwOVZhVm8yZUVsR1RXRXhaMFpvU2xGVk1GUjJkak5SSWl3aWEzUjVJam9pUlVNaUxDSjRJam9pTmxjM2RIRmFNWFpsVFRkWlNFNTVkRE4wT1hoTU9UZE1UMGh6WnpreWJtNXdYMlJzYVdOUWNWUmxjalJ0U1hoWmVUVnhNMDUyZVVrd2N6STFXa2xGT0NJc0lua2lPaUpEYTFSWFppMVlWVnA2TFZkMk56SXROWEJ2T1dkcGNuWjZTbmQ0TW5GVFEwa3labUY1YkhkWGNtdHRhbnBYTXpGRWNXRkhjalpGV2tRdGRWbERVMmRPSW4wc0luQjFjbkJ2YzJWeklqcGJJbUYxZEdobGJuUnBZMkYwYVc5dUlpd2lZWE56WlhKMGFXOXVUV1YwYUc5a0lsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhYMWRMQ0oxY0dSaGRHVkRiMjF0YVhSdFpXNTBJam9pUldsRE1taHRjMlJGTkhGT2QyMVRRMUJtTWpGMU1FeDZRa2h2YzNCV1VGRnNPVkp0VEdzM05GWjFaelZyZHlKOUxDSnpkV1ptYVhoRVlYUmhJanA3SW1SbGJIUmhTR0Z6YUNJNklrVnBRUzFaT1hWMFRVNWhhVWhWUjFwdE5UQm1RVkpqY1ZOYWNVZEJNemQ2UWxaVU5VWTRTMGx0WHpOVGRrRWlMQ0p5WldOdmRtVnllVU52YlcxcGRHMWxiblFpT2lKRmFVSlJiVU5pZGw5VlF6VlJSa04yT1VOd2IxSlBhVk01YkhaQ2RuZHVZMmxaTFY5NGNsRTRTVEp2UjBKbkluMHNJblI1Y0dVaU9pSmpjbVZoZEdVaWZRIiwKICAgICJuYW1lIjogIkpheWRlbiBEb2UiLAogICAgInNwb3VzZSI6ICJkaWQ6ZXhhbXBsZTpjMjc2ZTEyZWMyMWViZmViMWY3MTJlYmM2ZjEiCiAgfSwKICAiZXhwaXJhdGlvbkRhdGUiOiAiMjAyNS0wMy0xOFQyMjowNjozMS4zNDI1MDEwNloiLAogICJpZCI6ICJ1cm46dXVpZDo3Y2JiOWJhMi0zODAxLTQ3ODYtYjdjMS1hZDJkNDI5MjQzYTgiLAogICJpc3N1YW5jZURhdGUiOiAiMjAyNC0wMy0xOFQyMjowNjozMS4zNzU0MzQxMzFaIiwKICAiaXNzdWVyIjogewogICAgImlkIjogImRpZDppb246RWlBb2FISllGMlE2azl6aXloNFBEcnVzSVp0TlVLbTEtYUZyQXZyUERDRU1TUTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKaFpHUXRjSFZpYkdsakxXdGxlWE1pTENKd2RXSnNhV05MWlhseklqcGJleUpwWkNJNkltTTFOR1EwTWpnMkxXSXlOak10TkRJM01DMWhNRE14TFRVeU9USmlZelJqTmpSalpDSXNJbkIxWW14cFkwdGxlVXAzYXlJNmV5SmxJam9pUVZGQlFpSXNJbXRwWkNJNkltTTFOR1EwTWpnMkxXSXlOak10TkRJM01DMWhNRE14TFRVeU9USmlZelJqTmpSalpDSXNJbXQwZVNJNklsSlRRU0lzSW00aU9pSnVXVEJpTm1GR1RFNVZhbVEzZUVGcGJWcDRkVWd3Wms1U1RVY3paV05IV0c5WlMzUlJXRFIxUzFGamMxbFpVMjVHYUVGRGVEaG1hbkJuTmpKSmVWcENkVlJRWVRadVozTjJYMDVSUWtoVVdVbE1SakpSV1ZWUWFUUlFWRXN0ZVVFM1RtUlBTbFp3TlVGRlZ6VnljM2xzWlRnd1N5MVBVR2xVVVcxS1IyeDVVamhKVWtaSU5rOXRNM00yWVhOdFdWbDVaa2t0TFd0UlpUaElhMGQwYlhjM1RHbGlWV1ZaV25kM1J6bElTV3g2U0RSUU5taElUM05VWmxoaFJGbDJiRFUzV1dwMWJtZ3ljVlV5WVdsWFJGazVRV2xVVWtKcGJVeENhRWx1Wm5ZME5VNVllVGwxZDNKR09YTlpiRmh1YkdkNWJWVlRNVkZXY1RjNVZTMWxia3RtUlhNelIyczJSVVJpVjNKSGJYcEhjSFJQWkdwemRFa3dPV1Y0YVZSSU9FdHJkbmhPUldoUWVreGtlbmt3VUZObFJXWjRYMmxUV1d4TVpGbEpSa1ZVTjJveE1VNXZMVEZ5TWtWNlFrTnBVVFZ1ZEZJeE0xRWlmU3dpY0hWeWNHOXpaWE1pT2xzaVlYVjBhR1Z1ZEdsallYUnBiMjRpTENKaGMzTmxjblJwYjI1TlpYUm9iMlFpWFN3aWRIbHdaU0k2SWtwemIyNVhaV0pMWlhreU1ESXdJbjFkZlYwc0luVndaR0YwWlVOdmJXMXBkRzFsYm5RaU9pSkZhVU5FUmxGUVpuaFNSbk5zY2tGSWJWbzRhWE5RYkU0NFF6WktVbVJ6ZW5RMFpuSTBhRGxJTkdSbU1WWm5JbjBzSW5OMVptWnBlRVJoZEdFaU9uc2laR1ZzZEdGSVlYTm9Jam9pUldsQlNETXhORmRxUlROQmFUUmhMWE15U205SGNYWmpTMVZwVEVSZk1USlZaM1Z4WTBZMWJVZHpSblowWnlJc0luSmxZMjkyWlhKNVEyOXRiV2wwYldWdWRDSTZJa1ZwUVMxcGRrWnFUVFpMZG14MllVdFVPR2RDYTNCSmF6Wkxaa3h2T1VsNU1HbzBZV2htYWkxclQwbDJXbEVpZlN3aWRIbHdaU0k2SW1OeVpXRjBaU0o5IiwKICAgICJuYW1lIjogIkFjbWUgSXNzdWVyIgogIH0sCiAgInR5cGUiOiBbCiAgICAiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLAogICAgIlVuaXZlcnNpdHlEZWdyZWVDcmVkZW50aWFsIgogIF0KfQ== | @oidc4vc_rest_pre_auth_flow_trustlist_success Scenario Outline: OIDC credential issuance and verification Pre Auth flow with trustlist (Success) diff --git a/test/bdd/pkg/v1/oidc4vc/models.go b/test/bdd/pkg/v1/oidc4vc/models.go index de933c50f..4d2409673 100644 --- a/test/bdd/pkg/v1/oidc4vc/models.go +++ b/test/bdd/pkg/v1/oidc4vc/models.go @@ -10,6 +10,7 @@ import ( util "github.com/trustbloc/did-go/doc/util/time" vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" + "github.com/trustbloc/vcs/pkg/service/oidc4vp" ) type initiateOIDC4VCIResponse struct { @@ -109,7 +110,7 @@ type credentialMetadata struct { IssuanceDate *util.TimeWrapper `json:"issuanceDate,omitempty"` ExpirationDate *util.TimeWrapper `json:"expirationDate,omitempty"` CustomClaims map[string]map[string]interface{} `json:"customClaims,omitempty"` - Attachments []map[string]interface{} `json:"attachments,omitempty"` + Attachments []*oidc4vp.Attachment `json:"attachments,omitempty"` } type retrievedCredentialClaims map[string]credentialMetadata diff --git a/test/bdd/pkg/v1/oidc4vc/oidc4vci.go b/test/bdd/pkg/v1/oidc4vc/oidc4vci.go index 4ea52ea8b..d396f525b 100644 --- a/test/bdd/pkg/v1/oidc4vc/oidc4vci.go +++ b/test/bdd/pkg/v1/oidc4vc/oidc4vci.go @@ -344,6 +344,17 @@ func (s *Steps) setProofType(proofType string) { s.proofType = proofType } +func (s *Steps) setVPAttachments(rawJSON string) error { + attachments := make(map[string]string) + if err := json.Unmarshal([]byte(rawJSON), &attachments); err != nil { + return fmt.Errorf("unmarshal attachment: %w", err) + } + + s.vpAttachments = attachments + + return nil +} + func (s *Steps) setInitiateIssuanceVersion(version string) { s.initiateIssuanceApiVersion = version } diff --git a/test/bdd/pkg/v1/oidc4vc/oidc4vp.go b/test/bdd/pkg/v1/oidc4vc/oidc4vp.go index e450c562e..c1f943561 100644 --- a/test/bdd/pkg/v1/oidc4vc/oidc4vp.go +++ b/test/bdd/pkg/v1/oidc4vc/oidc4vp.go @@ -230,7 +230,7 @@ func (s *Steps) validateRetrievedCredentialClaims(claims retrievedCredentialClai var attachments []string for _, attachment := range val.Attachments { - attachments = append(attachments, attachment["uri"].(string)) + attachments = append(attachments, attachment.DataURI) } if len(s.expectedAttachment) > 0 { @@ -315,6 +315,10 @@ func (s *Steps) runOIDC4VPFlowWithOpts( oidc4vp.WithSchemaValidationDisabled(), } + if len(s.vpAttachments) > 0 { + opts = append(opts, oidc4vp.WithAttachments(s.vpAttachments)) + } + if useMultiVPs { opts = append(opts, oidc4vp.WithMultiVPs()) } diff --git a/test/bdd/pkg/v1/oidc4vc/steps.go b/test/bdd/pkg/v1/oidc4vc/steps.go index 7c5429993..5d56d5357 100644 --- a/test/bdd/pkg/v1/oidc4vc/steps.go +++ b/test/bdd/pkg/v1/oidc4vc/steps.go @@ -75,6 +75,7 @@ type Steps struct { composeCredential *verifiable.Credential expectedCredentialsAmountForVP int expectedAttachment []string + vpAttachments map[string]string } // NewSteps returns new Steps context. @@ -127,6 +128,7 @@ func (s *Steps) RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^Verifier with profile "([^"]*)" retrieves interactions claims$`, s.retrieveInteractionsClaim) sc.Step(`^Verifier with profile "([^"]*)" retrieves interactions claims with additional claims associated with custom scopes "([^"]*)"$`, s.retrieveInteractionsClaimWithCustomScopes) sc.Step(`^wallet configured to use hardcoded vp_token format "([^"]*)" for OIDC4VP interaction$`, s.setHardcodedVPTokenFormat) + sc.Step(`^wallet add attachments to vp flow with data '([^$]*)'$`, s.setVPAttachments) // Error cases sc.Step(`^User interacts with Wallet to initiate credential issuance using pre authorization code flow with invalid claims$`, s.runOIDC4VCIPreAuthWithInvalidClaims) @@ -167,6 +169,7 @@ func (s *Steps) ResetAndSetup() error { s.composeCredential = nil s.expectedCredentialsAmountForVP = 0 s.expectedAttachment = nil + s.vpAttachments = nil s.tlsConfig = s.bddContext.TLSConfig