Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support multi vps in vp_token by wallet cli #1739

Merged
merged 1 commit into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ func (f *Flow) getAttestationVP() (string, error) {
return "", fmt.Errorf("marshal presentation definition: %w", err)
}

presentations, err := f.wallet.Query(b, false)
presentations, _, err := f.wallet.Query(b, false, false)
if err != nil {
return "", fmt.Errorf("query wallet: %w", err)
}
Expand Down
177 changes: 111 additions & 66 deletions component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type Flow struct {
disableDomainMatching bool
disableSchemaValidation bool
perfInfo *PerfInfo
useMultiVPs bool
}

type provider interface {
Expand Down Expand Up @@ -150,6 +151,7 @@ func NewFlow(p provider, opts ...Opt) (*Flow, error) {
enableLinkedDomainVerification: o.enableLinkedDomainVerification,
disableDomainMatching: o.disableDomainMatching,
disableSchemaValidation: o.disableSchemaValidation,
useMultiVPs: o.useMultiVPs,
perfInfo: &PerfInfo{},
}, nil
}
Expand Down Expand Up @@ -194,32 +196,54 @@ func (f *Flow) Run(ctx context.Context) error {
requestObject.PresentationDefinition.InputDescriptors[0].Schema = nil
}

vp, err := f.queryWallet(&pd, requestObject.ClientMetadata.VPFormats)
vps, presentationSubmission, err := f.queryWallet(&pd, requestObject.ClientMetadata.VPFormats)
if err != nil {
return fmt.Errorf("query wallet: %w", err)
}

var attestationRequired bool
vpFormats := requestObject.ClientMetadata.VPFormats

if f.trustRegistry != nil && !reflect.ValueOf(f.trustRegistry).IsNil() {
attestationRequired, err = f.trustRegistry.ValidateVerifier(ctx, requestObject.ClientID, "", vp.Credentials())
if err != nil {
return fmt.Errorf("validate verifier: %w", err)
for i := range presentationSubmission.DescriptorMap {
if vpFormats.JwtVP != nil {
presentationSubmission.DescriptorMap[i].Format = "jwt_vp"
} else if vpFormats.LdpVP != nil {
presentationSubmission.DescriptorMap[i].Format = "ldp_vp"
}
}

if !f.disableDomainMatching {
credentials := vp.Credentials()
var credentials []*verifiable.Credential

for _, vp := range vps {
vpCredentials := vp.Credentials()

for i := len(credentials) - 1; i >= 0; i-- {
credential := credentials[i]
if !sameDIDWebDomain(credential.Contents().Issuer.ID, requestObject.ClientID) {
credentials = append(credentials[:i], credentials[i+1:]...)
if !f.disableDomainMatching {
for i := len(vpCredentials) - 1; i >= 0; i-- {
credential := vpCredentials[i]
if !sameDIDWebDomain(credential.Contents().Issuer.ID, requestObject.ClientID) {
vpCredentials = append(vpCredentials[:i], vpCredentials[i+1:]...)
}
}
}

credentials = append(credentials, vpCredentials...)
}

if err = f.sendAuthorizationResponse(ctx, requestObject, vp, attestationRequired); err != nil {
var attestationRequired bool

if f.trustRegistry != nil && !reflect.ValueOf(f.trustRegistry).IsNil() {
attestationRequired, err = f.trustRegistry.ValidateVerifier(ctx, requestObject.ClientID, "", credentials)
if err != nil {
return fmt.Errorf("validate verifier: %w", err)
}
}

if err = f.sendAuthorizationResponse(
ctx,
requestObject,
vps,
presentationSubmission,
attestationRequired,
); err != nil {
return fmt.Errorf("send authorization response: %w", err)
}

Expand Down Expand Up @@ -361,7 +385,7 @@ func getServiceType(serviceType interface{}) string {
func (f *Flow) queryWallet(
pd *presexch.PresentationDefinition,
vpFormat *presexch.Format,
) (*verifiable.Presentation, error) {
) ([]*verifiable.Presentation, *presexch.PresentationSubmission, error) {
slog.Info("Querying wallet")

start := time.Now()
Expand All @@ -371,19 +395,19 @@ func (f *Flow) queryWallet(

b, err := json.Marshal(pd)
if err != nil {
return nil, fmt.Errorf("marshal presentation definition: %w", err)
return nil, nil, fmt.Errorf("marshal presentation definition: %w", err)
}

presentations, err := f.wallet.Query(b, vpFormat.JwtVP != nil)
presentations, submission, err := f.wallet.Query(b, vpFormat.JwtVP != nil, f.useMultiVPs)
if err != nil {
return nil, err
return nil, nil, err
}

if len(presentations) == 0 || len(presentations[0].Credentials()) == 0 {
return nil, fmt.Errorf("no matching credentials found")
return nil, nil, fmt.Errorf("no matching credentials found")
}

return presentations[0], nil
return presentations, submission, nil
}

func sameDIDWebDomain(did1, did2 string) bool {
Expand All @@ -401,7 +425,8 @@ func sameDIDWebDomain(did1, did2 string) bool {
func (f *Flow) sendAuthorizationResponse(
ctx context.Context,
requestObject *RequestObject,
vp *verifiable.Presentation,
presentations []*verifiable.Presentation,
presentationSubmission *presexch.PresentationSubmission,
attestationRequired bool,
) error {
slog.Info("Sending authorization response",
Expand All @@ -410,25 +435,7 @@ func (f *Flow) sendAuthorizationResponse(

start := time.Now()

presentationSubmission, ok := vp.CustomFields["presentation_submission"].(*presexch.PresentationSubmission)
if !ok {
return fmt.Errorf("missing or invalid presentation_submission")
}

vpFormats := requestObject.ClientMetadata.VPFormats

for i := range presentationSubmission.DescriptorMap {
if vpFormats.JwtVP != nil {
presentationSubmission.DescriptorMap[i].Format = "jwt_vp"
} else if vpFormats.LdpVP != nil {
presentationSubmission.DescriptorMap[i].Format = "ldp_vp"
}
}

vpToken, err := f.createVPToken(vp, requestObject)
if err != nil {
return fmt.Errorf("create vp token: %w", err)
}
v := url.Values{}

idToken, err := f.createIDToken(
ctx,
Expand All @@ -439,55 +446,86 @@ func (f *Flow) sendAuthorizationResponse(
return fmt.Errorf("create id token: %w", err)
}

v.Add("id_token", idToken)

vpTokens, err := f.createVPToken(presentations, requestObject)
if err != nil {
return fmt.Errorf("create vp token: %w", err)
}

if len(vpTokens) == 1 {
v.Add("vp_token", vpTokens[0])
} else {
b, marshalErr := json.Marshal(vpTokens)
if marshalErr != nil {
return fmt.Errorf("marshal vp tokens: %w", marshalErr)
}

v.Add("vp_token", string(b))
}

presentationSubmissionJSON, err := json.Marshal(presentationSubmission)
if err != nil {
return fmt.Errorf("marshal presentation submission: %w", err)
}

v := url.Values{
"id_token": {idToken},
"vp_token": {vpToken},
"presentation_submission": {string(presentationSubmissionJSON)},
"state": {requestObject.State},
}
v.Add("presentation_submission", string(presentationSubmissionJSON))
v.Add("state", requestObject.State)

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

return f.postAuthorizationResponse(ctx, requestObject.ResponseURI, []byte(v.Encode()))
}

func (f *Flow) createVPToken(
presentation *verifiable.Presentation,
presentations []*verifiable.Presentation,
requestObject *RequestObject,
) (string, error) {
credential := presentation.Credentials()[0]
) ([]string, error) {
credential := presentations[0].Credentials()[0]

subjectDID, err := verifiable.SubjectID(credential.Contents().Subject)
if err != nil {
return "", fmt.Errorf("get subject did: %w", err)
return nil, fmt.Errorf("get subject did: %w", err)
}

vpFormats := requestObject.ClientMetadata.VPFormats

switch {
case vpFormats.JwtVP != nil:
return f.signPresentationJWT(
presentation,
subjectDID,
requestObject.ClientID,
requestObject.Nonce,
)
case vpFormats.LdpVP != nil:
return f.signPresentationLDP(
presentation,
vcs.SignatureType(vpFormats.LdpVP.ProofType[0]),
subjectDID,
requestObject.ClientID,
requestObject.Nonce,
var vpTokens []string

for _, presentation := range presentations {
var (
vpToken string
signErr error
)
default:
return "", fmt.Errorf("no supported vp formats: %v", vpFormats)

switch {
case vpFormats.JwtVP != nil:
if vpToken, signErr = f.signPresentationJWT(
presentation,
subjectDID,
requestObject.ClientID,
requestObject.Nonce,
); signErr != nil {
return nil, signErr
}
case vpFormats.LdpVP != nil:
if vpToken, signErr = f.signPresentationLDP(
presentation,
vcs.SignatureType(vpFormats.LdpVP.ProofType[0]),
subjectDID,
requestObject.ClientID,
requestObject.Nonce,
); signErr != nil {
return nil, signErr
}
default:
return nil, fmt.Errorf("unsupported vp formats: %v", vpFormats)
}

vpTokens = append(vpTokens, vpToken)
}

return vpTokens, nil
}

func (f *Flow) signPresentationJWT(
Expand Down Expand Up @@ -743,6 +781,7 @@ type options struct {
enableLinkedDomainVerification bool
disableDomainMatching bool
disableSchemaValidation bool
useMultiVPs bool
}

type Opt func(opts *options)
Expand Down Expand Up @@ -776,3 +815,9 @@ func WithSchemaValidationDisabled() Opt {
opts.disableSchemaValidation = true
}
}

func WithMultiVPs() Opt {
return func(opts *options) {
opts.useMultiVPs = true
}
}
42 changes: 33 additions & 9 deletions component/wallet-cli/pkg/wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,25 +423,29 @@ func (w *Wallet) GetAll() (map[string]json.RawMessage, error) {
}

// Query runs the given presentation definition on the stored credentials.
func (w *Wallet) Query(pdBytes []byte, jwtVPFormat bool) ([]*verifiable.Presentation, error) {
func (w *Wallet) Query(
pdBytes []byte,
jwtVPFormat bool,
useMultiVPs bool,
) ([]*verifiable.Presentation, *presexch.PresentationSubmission, error) {
vcContent, err := w.GetAll()
if err != nil {
return nil, fmt.Errorf("query credentials: %w", err)
return nil, nil, fmt.Errorf("query credentials: %w", err)
}

if len(vcContent) == 0 {
return nil, fmt.Errorf("no credentials found in wallet")
return nil, nil, fmt.Errorf("no credentials found in wallet")
}

credentials, err := parseCredentialContents(vcContent, w.documentLoader)
if err != nil {
return nil, err
return nil, nil, err
}

var pd presexch.PresentationDefinition

if err = json.Unmarshal(pdBytes, &pd); err != nil {
return nil, err
return nil, nil, err
}

opts := []presexch.MatchRequirementsOpt{
Expand All @@ -455,16 +459,36 @@ func (w *Wallet) Query(pdBytes []byte, jwtVPFormat bool) ([]*verifiable.Presenta
opts = append(opts, presexch.WithDefaultPresentationFormat(presexch.FormatJWTVP))
}

vp, err := pd.CreateVP(credentials, w.documentLoader, opts...)
if useMultiVPs {
vps, presentationSubmission, createErr := pd.CreateVPArray(credentials, w.documentLoader, opts...)
if createErr != nil {
if errors.Is(createErr, presexch.ErrNoCredentials) {
return nil, nil, fmt.Errorf("no matching credentials found")
}

return nil, nil, createErr
}

return vps, presentationSubmission, nil
}

var vp *verifiable.Presentation

vp, err = pd.CreateVP(credentials, w.documentLoader, opts...)
if err != nil {
if errors.Is(err, presexch.ErrNoCredentials) {
return nil, fmt.Errorf("no matching credentials found")
return nil, nil, fmt.Errorf("no matching credentials found")
}

return nil, err
return nil, nil, err
}

presentationSubmission, ok := vp.CustomFields["presentation_submission"].(*presexch.PresentationSubmission)
if !ok {
return nil, nil, fmt.Errorf("missing or invalid presentation_submission")
}

return []*verifiable.Presentation{vp}, nil
return []*verifiable.Presentation{vp}, presentationSubmission, nil
}

func parseCredentialContents(m map[string]json.RawMessage, loader ld.DocumentLoader) ([]*verifiable.Credential, error) {
Expand Down
15 changes: 15 additions & 0 deletions test/bdd/features/oidc4vc_api.feature
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,18 @@ Feature: OIDC4VC REST API
And Verifier with profile "v_myprofile_jwt_client_attestation/v1.0" retrieves interactions claims
Then we wait 2 seconds
And Verifier with profile "v_myprofile_jwt_client_attestation/v1.0" requests deleted interactions claims

@oidc4vc_rest_multi_vp
Scenario: OIDC credential pre-authorized code flow issuance and verification with multiple VPs
Given Profile "bank_issuer/v1.0" issuer has been authorized with username "profile-user-issuer-1" and password "profile-user-issuer-1-pwd"
And User holds credential "UniversityDegreeCredential,VerifiedEmployee" with templateID "nil"
And User wants to make credentials request based on credential offer "false"
And Profile "v_myprofile_multivp_jwt/v1.0" verifier has been authorized with username "profile-user-verifier-1" and password "profile-user-verifier-1-pwd"

When User interacts with Wallet to initiate batch credential issuance using pre authorization code flow
Then "2" credentials are issued
Then expected credential count for vp flow is "2"
Then User interacts with Verifier and initiate OIDC4VP interaction under "v_myprofile_multivp_jwt/v1.0" profile with presentation definition ID "8bc45260-ed00-4c23-a32a-b70e5aef3d92" and fields "degree_type_id,verified_employee_id" using multi vps
And Verifier with profile "v_myprofile_multivp_jwt/v1.0" retrieves interactions claims
Then we wait 2 seconds
And Verifier with profile "v_myprofile_multivp_jwt/v1.0" requests deleted interactions claims
Loading
Loading