/* * Copyright (C) 2024 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 holder import ( "context" "encoding/json" "errors" "fmt" "strings" "time" "github.com/google/uuid" "github.com/lestrrat-go/jwx/v2/jws" "github.com/lestrrat-go/jwx/v2/jwt" 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/auth/oauth" "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vcr/signature" "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/piprate/json-gold/ld" ) type presenter struct { documentLoader ld.DocumentLoader signer crypto.JWTSigner keyResolver resolver.KeyResolver } func (p presenter) buildSubmission(ctx context.Context, credentials map[did.DID][]vc.VerifiableCredential, presentationDefinition pe.PresentationDefinition, params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { // match against the wallet's credentials // if there's a match, create a VP and call the token endpoint // If the token endpoint succeeds, return the access token // If no presentation definition matches, return a 412 "no matching credentials" error builder := presentationDefinition.PresentationSubmissionBuilder() for holderDID, creds := range credentials { builder.AddWallet(holderDID, creds) } // Find supported VP format, matching support from: // - what the local Nuts node supports // - the presentation definition "claimed format designation" (optional) // - the verifier's metadata (optional) formatCandidates := credential.OpenIDSupportedFormats(oauth.DefaultOpenIDSupportedFormats()) formatCandidates = formatCandidates.Match(credential.OpenIDSupportedFormats(params.Format)) if presentationDefinition.Format != nil { formatCandidates = formatCandidates.Match(credential.DIFClaimFormats(*presentationDefinition.Format)) } // todo: next to the format selection, also check for algorithm support format := pe.ChooseVPFormat(formatCandidates.Map) if format == "" { return nil, nil, errors.New("requester, verifier (authorization server metadata) and presentation definition don't share a supported VP format") } presentationSubmission, signInstruction, err := builder.Build(format) if err != nil { return nil, nil, err } holderDID := signInstruction.Holder.URI() vp, err := p.buildPresentation(ctx, &signInstruction.Holder, signInstruction.VerifiableCredentials, PresentationOptions{ Format: format, Holder: &holderDID, ProofOptions: proof.ProofOptions{ Created: time.Now(), Challenge: ¶ms.Nonce, Domain: ¶ms.Audience, Expires: ¶ms.Expires, Nonce: ¶ms.Nonce, }, }) if err != nil { return nil, nil, fmt.Errorf("failed to create verifiable presentation: %w", err) } return vp, &presentationSubmission, nil } func (p presenter) buildPresentation(ctx context.Context, signerDID *did.DID, credentials []vc.VerifiableCredential, options PresentationOptions) (*vc.VerifiablePresentation, error) { var err error if signerDID == nil { signerDID, err = credential.ResolveSubjectDID(credentials...) if err != nil { return nil, fmt.Errorf("unable to resolve signer DID from VCs for creating VP: %w", err) } } kid, _, err := p.keyResolver.ResolveKey(*signerDID, nil, resolver.NutsSigningKeyType) if err != nil { return nil, fmt.Errorf("unable to resolve assertion key for signing VP (did=%s): %w", *signerDID, err) } switch options.Format { case JWTPresentationFormat: return p.buildJWTPresentation(ctx, *signerDID, credentials, options, kid) case "": fallthrough case JSONLDPresentationFormat: return p.buildJSONLDPresentation(ctx, *signerDID, credentials, options, kid) default: return nil, fmt.Errorf("unsupported presentation proof format: %s", options.Format) } } // buildJWTPresentation builds a JWT presentation according to https://www.w3.org/TR/vc-data-model/#json-web-token func (p presenter) buildJWTPresentation(ctx context.Context, subjectDID did.DID, credentials []vc.VerifiableCredential, options PresentationOptions, keyID string) (*vc.VerifiablePresentation, error) { headers := map[string]interface{}{ jws.TypeKey: "JWT", } id := did.DIDURL{DID: subjectDID} id.Fragment = strings.ToLower(uuid.NewString()) type VPAlias vc.VerifiablePresentation claims := map[string]interface{}{ jwt.SubjectKey: subjectDID.String(), jwt.JwtIDKey: id.String(), "vp": VPAlias(vc.VerifiablePresentation{ Context: append([]ssi.URI{VerifiableCredentialLDContextV1}, options.AdditionalContexts...), Type: append([]ssi.URI{VerifiablePresentationLDType}, options.AdditionalTypes...), Holder: options.Holder, VerifiableCredential: credentials, }), } if options.ProofOptions.Nonce != nil { claims["nonce"] = *options.ProofOptions.Nonce } if options.ProofOptions.Domain != nil { claims[jwt.AudienceKey] = *options.ProofOptions.Domain } if options.ProofOptions.Created.IsZero() { claims[jwt.NotBeforeKey] = time.Now().Unix() } else { claims[jwt.NotBeforeKey] = int(options.ProofOptions.Created.Unix()) } if options.ProofOptions.Expires != nil { claims[jwt.ExpirationKey] = int(options.ProofOptions.Expires.Unix()) } for claimName, value := range options.ProofOptions.AdditionalProperties { claims[claimName] = value } token, err := p.signer.SignJWT(ctx, claims, headers, keyID) if err != nil { return nil, fmt.Errorf("unable to sign JWT presentation: %w", err) } return vc.ParseVerifiablePresentation(token) } func (p presenter) buildJSONLDPresentation(ctx context.Context, subjectDID did.DID, credentials []vc.VerifiableCredential, options PresentationOptions, keyID string) (*vc.VerifiablePresentation, error) { ldContext := []ssi.URI{VerifiableCredentialLDContextV1, signature.JSONWebSignature2020Context} ldContext = append(ldContext, options.AdditionalContexts...) types := []ssi.URI{VerifiablePresentationLDType} types = append(types, options.AdditionalTypes...) id := did.DIDURL{DID: subjectDID} id.Fragment = strings.ToLower(uuid.NewString()) idURI := id.URI() unsignedVP := &vc.VerifiablePresentation{ ID: &idURI, Context: ldContext, Type: types, Holder: options.Holder, VerifiableCredential: credentials, } // Convert to map[string]interface{} for signing documentBytes, err := unsignedVP.MarshalJSON() if err != nil { return nil, err } var document proof.Document err = json.Unmarshal(documentBytes, &document) if err != nil { return nil, err } ldProof := proof.NewLDProof(options.ProofOptions) signingResult, err := ldProof. Sign(ctx, document, signature.JSONWebSignature2020{ContextLoader: p.documentLoader, Signer: p.signer}, keyID) if err != nil { return nil, fmt.Errorf("unable to sign VP with LD proof: %w", err) } resultJSON, _ := json.Marshal(signingResult) return vc.ParseVerifiablePresentation(string(resultJSON)) }