diff --git a/auth/client/iam/openid4vp.go b/auth/client/iam/openid4vp.go index 0f9e370601..3f9ef0dc79 100644 --- a/auth/client/iam/openid4vp.go +++ b/auth/client/iam/openid4vp.go @@ -24,16 +24,17 @@ import ( "encoding/json" "errors" "fmt" - "github.com/nuts-foundation/nuts-node/http/client" - "github.com/nuts-foundation/nuts-node/vcr/credential" - "github.com/nuts-foundation/nuts-node/vdr/didsubject" - "github.com/piprate/json-gold/ld" "maps" "net/http" "net/url" "slices" "time" + "github.com/nuts-foundation/nuts-node/http/client" + "github.com/nuts-foundation/nuts-node/vcr/credential" + "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/piprate/json-gold/ld" + "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth/log" diff --git a/go.mod b/go.mod index 19df4726a0..0401b4fbe4 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/nats-io/nats-server/v2 v2.11.15 github.com/nats-io/nats.go v1.49.0 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b - github.com/nuts-foundation/go-did v0.18.0 + github.com/nuts-foundation/go-did v0.18.1 github.com/nuts-foundation/go-leia/v4 v4.3.0 github.com/nuts-foundation/go-stoabs v1.11.0 github.com/nuts-foundation/sqlite v1.0.0 diff --git a/go.sum b/go.sum index 78b49d7b4d..4531fbca49 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,8 @@ github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0m github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= +github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= @@ -144,6 +146,8 @@ github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/eknkc/basex v1.0.1 h1:TcyAkqh4oJXgV3WYyL4KEfCMk9W8oJCpmx1bo+jVgKY= github.com/eknkc/basex v1.0.1/go.mod h1:k/F/exNEHFdbs3ZHuasoP2E7zeWwZblG84Y7Z59vQRo= +github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw= +github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M= github.com/eko/gocache/lib/v4 v4.2.3 h1:s78TFqEGAH3SbzP4N40D755JYT/aaGFKEPrsUtC1chU= github.com/eko/gocache/lib/v4 v4.2.3/go.mod h1:Zus8mwmaPu1VYOzfomb+Dvx2wV7fT5jDRbHYtQM6MEY= github.com/eko/gocache/store/go_cache/v4 v4.2.2 h1:tAI9nl6TLoJyKG1ujF0CS0n/IgTEMl+NivxtR5R3/hw= @@ -404,6 +408,8 @@ github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b h1:80 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b/go.mod h1:6YUioYirD6/8IahZkoS4Ypc8xbeJW76Xdk1QKcziNTM= github.com/nuts-foundation/go-did v0.18.0 h1:IB0X8PrzDulpR1zAgDpaHfwoSjJpIhx5u1Tg8I2nnb8= github.com/nuts-foundation/go-did v0.18.0/go.mod h1:4od1gAmCi9HjHTQGEvHC8pLeuXdXACxidAcdA52YScc= +github.com/nuts-foundation/go-did v0.18.1 h1:oMEF42ckWcOG3bD0KGu1RAKByQx4vryPJxl6L9zJyQA= +github.com/nuts-foundation/go-did v0.18.1/go.mod h1:4od1gAmCi9HjHTQGEvHC8pLeuXdXACxidAcdA52YScc= github.com/nuts-foundation/go-leia/v4 v4.3.0 h1:R0qGISIeg2q/PCQTC9cuoBtA6cFu4WBV2DbmSOWKZyM= github.com/nuts-foundation/go-leia/v4 v4.3.0/go.mod h1:Gw6bXqJLOAmHSiXJJYbVoj+Mowp/PoBRywO0ZPsVzA0= github.com/nuts-foundation/go-stoabs v1.11.0 h1:q18jVruPdFcVhodDrnKuhq/24i0pUC/YXgzJS0glKUU= @@ -779,6 +785,8 @@ gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXD gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI= gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs= +gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= diff --git a/vcr/holder/presenter.go b/vcr/holder/presenter.go index 182f434d41..b5f549b910 100644 --- a/vcr/holder/presenter.go +++ b/vcr/holder/presenter.go @@ -23,9 +23,10 @@ import ( "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" @@ -37,8 +38,6 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/piprate/json-gold/ld" - "strings" - "time" ) type presenter struct { @@ -123,43 +122,18 @@ func (p presenter) buildPresentation(ctx context.Context, signerDID *did.DID, cr // 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()) - claims := map[string]interface{}{ - jwt.SubjectKey: subjectDID.String(), - jwt.JwtIDKey: id.String(), - "vp": 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) + return vc.CreateJWTVerifiablePresentation(ctx, subjectDID.URI(), credentials, vc.PresentationOptions{ + AdditionalContexts: options.AdditionalContexts, + AdditionalTypes: options.AdditionalTypes, + AdditionalProofProperties: options.ProofOptions.AdditionalProperties, + Holder: options.Holder, + Nonce: options.ProofOptions.Nonce, + Audience: options.ProofOptions.Domain, + IssuedAt: &options.ProofOptions.Created, + ExpiresAt: options.ProofOptions.Expires, + }, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) { + return p.signer.SignJWT(ctx, claims, headers, keyID) + }) } func (p presenter) buildJSONLDPresentation(ctx context.Context, subjectDID did.DID, credentials []vc.VerifiableCredential, options PresentationOptions, keyID string) (*vc.VerifiablePresentation, error) { diff --git a/vcr/holder/presenter_test.go b/vcr/holder/presenter_test.go index efc47e70b8..a6d361899a 100644 --- a/vcr/holder/presenter_test.go +++ b/vcr/holder/presenter_test.go @@ -20,6 +20,9 @@ package holder import ( "context" + "testing" + "time" + ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -39,8 +42,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "testing" - "time" ) func TestPresenter_buildPresentation(t *testing.T) { @@ -142,7 +143,7 @@ func TestPresenter_buildPresentation(t *testing.T) { }) }) t.Run("JWT", func(t *testing.T) { - options := PresentationOptions{Format: JWTPresentationFormat} + options := PresentationOptions{Format: JWTPresentationFormat, ProofOptions: proof.ProofOptions{Created: time.Now()}} t.Run("ok - one VC", func(t *testing.T) { ctrl := gomock.NewController(t) @@ -162,7 +163,33 @@ func TestPresenter_buildPresentation(t *testing.T) { assert.NotNil(t, result.JWT()) nonce, _ := result.JWT().Get("nonce") assert.Empty(t, nonce) + + t.Run("#3957: Verifiable Presentation type is marshalled incorrectly in JWT format", func(t *testing.T) { + t.Run("make sure the fix is backwards compatible", func(t *testing.T) { + vp := vc.VerifiablePresentation{ + Type: []ssi.URI{ssi.MustParseURI("VerifiablePresentation")}, + } + t.Run("sanity check: regular JSON marshalling yields type: string", func(t *testing.T) { + data, err := vp.MarshalJSON() + require.NoError(t, err) + assert.Contains(t, string(data), `"type":"VerifiablePresentation"`) + }) + }) + vpAsMap := result.JWT().PrivateClaims()["vp"].(map[string]any) + t.Run("make sure type now marshals as array", func(t *testing.T) { + typeProp := vpAsMap["type"].([]any) + assert.Equal(t, []any{"VerifiablePresentation"}, typeProp) + }) + t.Run("make sure the VP can be unmarshalled", func(t *testing.T) { + presentation, err := vc.ParseVerifiablePresentation(result.Raw()) + require.NoError(t, err) + assert.Equal(t, result.ID.String(), presentation.ID.String()) + assert.Len(t, presentation.Type, 1) + assert.Equal(t, "VerifiablePresentation", presentation.Type[0].String()) + }) + }) }) + t.Run("ok - multiple VCs", func(t *testing.T) { ctrl := gomock.NewController(t) diff --git a/vcr/pe/presentation_submission.go b/vcr/pe/presentation_submission.go index 1d78ed137f..2adb9391c9 100644 --- a/vcr/pe/presentation_submission.go +++ b/vcr/pe/presentation_submission.go @@ -22,13 +22,14 @@ import ( "encoding/json" "errors" "fmt" + "strings" + "github.com/PaesslerAG/jsonpath" "github.com/google/uuid" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/vcr/credential" v2 "github.com/nuts-foundation/nuts-node/vcr/pe/schema/v2" - "strings" ) // ParsePresentationSubmission validates the given JSON and parses it into a PresentationSubmission. @@ -133,11 +134,15 @@ func (b *PresentationSubmissionBuilder) Build(format string) (PresentationSubmis } // the verifiableCredential property in Verifiable Presentations can be a single VC or an array of VCs when represented in JSON. - // go-did always marshals a single VC as a single VC for JSON-LD VPs. So we might need to fix the mapping paths. - - // todo the check below actually depends on the format of the credential and not the format of the VP + // go-did always marshals a single VC as a single VC for JSON-LD VPs. So we need to fix the mapping paths. if len(signInstruction.Mappings) == 1 { - signInstruction.Mappings[0].Path = "$.verifiableCredential" + if format == vc.JWTPresentationProofFormat { + // JWT VP always has an array of VCs + signInstruction.Mappings[0].Path = "$.verifiableCredential[0]" + } else { + // JSON-LD VP with single VC has single VC in verifiableCredential + signInstruction.Mappings[0].Path = "$.verifiableCredential" + } } // Just 1 VP, no nesting needed diff --git a/vcr/pe/presentation_submission_test.go b/vcr/pe/presentation_submission_test.go index 5388e234ef..b85e60afb9 100644 --- a/vcr/pe/presentation_submission_test.go +++ b/vcr/pe/presentation_submission_test.go @@ -20,6 +20,9 @@ package pe import ( "encoding/json" + "testing" + "time" + ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -27,8 +30,6 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" - "time" ) func TestParsePresentationSubmission(t *testing.T) { @@ -189,7 +190,7 @@ func TestPresentationSubmissionBuilder_Build(t *testing.T) { assert.Len(t, signInstruction.VerifiableCredentials, 1) assert.Equal(t, holder1, signInstruction.Holder) require.Len(t, submission.DescriptorMap, 1) - assert.Equal(t, "$.verifiableCredential", submission.DescriptorMap[0].Path) + assert.Equal(t, "$.verifiableCredential[0]", submission.DescriptorMap[0].Path) }) }) }