Skip to content

Commit

Permalink
feat: support multi vps in vp_token by wallet cli
Browse files Browse the repository at this point in the history
Signed-off-by: Andrii Holovko <[email protected]>
  • Loading branch information
aholovko committed Jul 2, 2024
1 parent 4e3c76e commit 8985d2e
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 107 deletions.
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

0 comments on commit 8985d2e

Please sign in to comment.