From e55c706c51737ff6d83f2f3ab66058855f1ba3a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:07:13 +0000 Subject: [PATCH 1/5] Initial plan From 530e2eac6dfc68e0a1885bc996c48a676302c713 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:18:54 +0000 Subject: [PATCH 2/5] Improve error messages for DID resolution failures in VP verification Co-authored-by: reinkrul <1481228+reinkrul@users.noreply.github.com> --- auth/api/iam/openid4vp.go | 32 +++++++++++++++++++++++++- auth/api/iam/openid4vp_test.go | 41 ++++++++++++++++++++++++++++++++++ auth/api/iam/s2s_vptoken.go | 2 +- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index 405a52e7b3..01cd029434 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -536,7 +536,7 @@ func (r Wrapper) handleAuthorizeResponseSubmission(ctx context.Context, request if err != nil { return nil, oauth.OAuth2Error{ Code: oauth.InvalidRequest, - Description: "presentation(s) or contained credential(s) are invalid", + Description: verificationErrorDescription(err), InternalError: err, RedirectURI: callbackURI, } @@ -764,3 +764,33 @@ func oauthError(code oauth.ErrorCode, description string, internalError ...error InternalError: errors.Join(internalError...), } } + +// verificationErrorDescription returns a more specific error description when DID resolution fails, +// otherwise returns the generic error message. This improves user experience by providing actionable +// error information for common DID resolution issues while maintaining security for other errors. +func verificationErrorDescription(err error) string { + errMsg := err.Error() + + // Check for wrapped DID resolution errors first (e.g., "unable to resolve valid signing key: ...") + // These contain more context and should be preferred over the specific error checks + if strings.Contains(errMsg, "unable to resolve") { + return "presentation(s) or contained credential(s) are invalid: " + errMsg + } + + // Check error messages directly since ErrDeactivated and ErrNoActiveController both implement deactivatedError + if strings.Contains(errMsg, "no active controllers") { + return "presentation(s) or contained credential(s) are invalid: DID document has no active controllers" + } + if strings.Contains(errMsg, "deactivated") { + return "presentation(s) or contained credential(s) are invalid: DID document has been deactivated" + } + if errors.Is(err, resolver.ErrNotFound) { + return "presentation(s) or contained credential(s) are invalid: unable to resolve DID document" + } + if errors.Is(err, resolver.ErrKeyNotFound) { + return "presentation(s) or contained credential(s) are invalid: signing key not found in DID document" + } + + // Default generic message for other errors + return "presentation(s) or contained credential(s) are invalid" +} diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index f57ff74a73..0a347479ab 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -21,6 +21,8 @@ package iam import ( "context" "encoding/json" + "errors" + "fmt" "github.com/nuts-foundation/nuts-node/http/user" "net/http" "net/url" @@ -35,6 +37,7 @@ import ( "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/test" "github.com/nuts-foundation/nuts-node/vcr/pe" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -964,5 +967,43 @@ func putNonce(ctx *testCtx, nonce string) { func putCodeSession(ctx *testCtx, code string, oauthSession OAuthSession) { _ = ctx.client.oauthCodeStore().Put(code, oauthSession) +} + +func Test_verificationErrorDescription(t *testing.T) { + t.Run("DID not found error", func(t *testing.T) { + err := resolver.ErrNotFound + result := verificationErrorDescription(err) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid: unable to resolve DID document", result) + }) + + t.Run("DID deactivated error", func(t *testing.T) { + err := resolver.ErrDeactivated + result := verificationErrorDescription(err) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid: DID document has been deactivated", result) + }) + + t.Run("No active controller error", func(t *testing.T) { + err := resolver.ErrNoActiveController + result := verificationErrorDescription(err) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid: DID document has no active controllers", result) + }) + t.Run("Key not found error", func(t *testing.T) { + err := resolver.ErrKeyNotFound + result := verificationErrorDescription(err) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid: signing key not found in DID document", result) + }) + + t.Run("Wrapped DID resolution error", func(t *testing.T) { + err := fmt.Errorf("unable to resolve valid signing key: %w", resolver.ErrNotFound) + result := verificationErrorDescription(err) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid: unable to resolve valid signing key: unable to find the DID document", result) + }) + + t.Run("Generic error returns default message", func(t *testing.T) { + err := errors.New("some other error") + result := verificationErrorDescription(err) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid", result) + }) } + diff --git a/auth/api/iam/s2s_vptoken.go b/auth/api/iam/s2s_vptoken.go index 16018dbc31..c215ea4269 100644 --- a/auth/api/iam/s2s_vptoken.go +++ b/auth/api/iam/s2s_vptoken.go @@ -101,7 +101,7 @@ func (r Wrapper) handleS2SAccessTokenRequest(ctx context.Context, clientID strin if err != nil { return nil, oauth.OAuth2Error{ Code: oauth.InvalidRequest, - Description: "presentation(s) or contained credential(s) are invalid", + Description: verificationErrorDescription(err), InternalError: err, } } From 8ea7b942be5e442e5fc47bc66093eabc1b79789b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:24:33 +0000 Subject: [PATCH 3/5] Complete implementation - all tests pass, security check passed Co-authored-by: reinkrul <1481228+reinkrul@users.noreply.github.com> --- vcr/pe/schema/gen/go.mod | 4 +--- vcr/pe/schema/gen/go.sum | 14 -------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/vcr/pe/schema/gen/go.mod b/vcr/pe/schema/gen/go.mod index b872a8ae8e..05c918dc65 100644 --- a/vcr/pe/schema/gen/go.mod +++ b/vcr/pe/schema/gen/go.mod @@ -2,6 +2,4 @@ module github.com/nuts-foundation/nuts-node/vcr/pe/gen/schema go 1.21 -require ( - github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60 // indirect -) +require github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60 diff --git a/vcr/pe/schema/gen/go.sum b/vcr/pe/schema/gen/go.sum index 1f994a18a1..d5af305fbb 100644 --- a/vcr/pe/schema/gen/go.sum +++ b/vcr/pe/schema/gen/go.sum @@ -1,16 +1,2 @@ -github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= -github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= -github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E= -github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= -github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= -github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= -github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= -github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97 h1:XIsQOSBJi/9Bexr+rjUpuYi0IkQ+YqNKKlE7Yt/sw9Q= -github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97/go.mod h1:zTyVtYhYjcHpfCtqnCMxejgp0pEEwb/xJzhn05NrkJk= github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60 h1:/rNdG6EuzjwcR1KRFpF+9qWmWh2xIcz84QOeMGr/2L8= github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60/go.mod h1:traiLYQ0YD7qUMCdjo6/jSaJRPHXniX4HVs+PhEhYpc= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From a9b37f2fe0f8293a669fc2b7d9b2a46dfe5508a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:05:42 +0000 Subject: [PATCH 4/5] Simplify verificationErrorDescription based on review feedback - Remove legacy did:nuts error checks (deactivated, no active controllers) - Simplify to just append err.Error() for DID resolution errors - Update tests to match simplified implementation Co-authored-by: reinkrul <1481228+reinkrul@users.noreply.github.com> --- auth/api/iam/openid4vp.go | 23 +++-------------------- auth/api/iam/openid4vp_test.go | 16 ++-------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index 01cd029434..7644032adc 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -769,26 +769,9 @@ func oauthError(code oauth.ErrorCode, description string, internalError ...error // otherwise returns the generic error message. This improves user experience by providing actionable // error information for common DID resolution issues while maintaining security for other errors. func verificationErrorDescription(err error) string { - errMsg := err.Error() - - // Check for wrapped DID resolution errors first (e.g., "unable to resolve valid signing key: ...") - // These contain more context and should be preferred over the specific error checks - if strings.Contains(errMsg, "unable to resolve") { - return "presentation(s) or contained credential(s) are invalid: " + errMsg - } - - // Check error messages directly since ErrDeactivated and ErrNoActiveController both implement deactivatedError - if strings.Contains(errMsg, "no active controllers") { - return "presentation(s) or contained credential(s) are invalid: DID document has no active controllers" - } - if strings.Contains(errMsg, "deactivated") { - return "presentation(s) or contained credential(s) are invalid: DID document has been deactivated" - } - if errors.Is(err, resolver.ErrNotFound) { - return "presentation(s) or contained credential(s) are invalid: unable to resolve DID document" - } - if errors.Is(err, resolver.ErrKeyNotFound) { - return "presentation(s) or contained credential(s) are invalid: signing key not found in DID document" + // Check for DID resolution errors + if errors.Is(err, resolver.ErrNotFound) || errors.Is(err, resolver.ErrKeyNotFound) || strings.Contains(err.Error(), "unable to resolve") { + return "presentation(s) or contained credential(s) are invalid: " + err.Error() } // Default generic message for other errors diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index 0a347479ab..c604ac1afc 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -973,25 +973,13 @@ func Test_verificationErrorDescription(t *testing.T) { t.Run("DID not found error", func(t *testing.T) { err := resolver.ErrNotFound result := verificationErrorDescription(err) - assert.Equal(t, "presentation(s) or contained credential(s) are invalid: unable to resolve DID document", result) - }) - - t.Run("DID deactivated error", func(t *testing.T) { - err := resolver.ErrDeactivated - result := verificationErrorDescription(err) - assert.Equal(t, "presentation(s) or contained credential(s) are invalid: DID document has been deactivated", result) - }) - - t.Run("No active controller error", func(t *testing.T) { - err := resolver.ErrNoActiveController - result := verificationErrorDescription(err) - assert.Equal(t, "presentation(s) or contained credential(s) are invalid: DID document has no active controllers", result) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid: unable to find the DID document", result) }) t.Run("Key not found error", func(t *testing.T) { err := resolver.ErrKeyNotFound result := verificationErrorDescription(err) - assert.Equal(t, "presentation(s) or contained credential(s) are invalid: signing key not found in DID document", result) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid: key not found in DID document", result) }) t.Run("Wrapped DID resolution error", func(t *testing.T) { From e3c85f5b8eb5ddab46e1a38ac689bc8659b03126 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 30 Jan 2026 15:17:09 +0100 Subject: [PATCH 5/5] wip --- auth/api/auth/v1/api_test.go | 4 ++++ auth/api/iam/openid4vp.go | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/auth/api/auth/v1/api_test.go b/auth/api/auth/v1/api_test.go index 3310ad2ba9..17354201fe 100644 --- a/auth/api/auth/v1/api_test.go +++ b/auth/api/auth/v1/api_test.go @@ -74,6 +74,10 @@ func (m *mockAuthClient) AuthorizationEndpointEnabled() bool { return true } +func (m *mockAuthClient) OpenID4VCIEnabled() bool { + return true +} + func (m *mockAuthClient) AuthzServer() oauth.AuthorizationServer { return m.authzServer } diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index 7644032adc..1493fd2eac 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -23,7 +23,10 @@ import ( "encoding/json" "errors" "fmt" + "github.com/nuts-foundation/nuts-node/http/user" + "github.com/nuts-foundation/nuts-node/vcr/types" + "net/http" "net/url" "slices" @@ -770,10 +773,11 @@ func oauthError(code oauth.ErrorCode, description string, internalError ...error // error information for common DID resolution issues while maintaining security for other errors. func verificationErrorDescription(err error) string { // Check for DID resolution errors - if errors.Is(err, resolver.ErrNotFound) || errors.Is(err, resolver.ErrKeyNotFound) || strings.Contains(err.Error(), "unable to resolve") { - return "presentation(s) or contained credential(s) are invalid: " + err.Error() + if errors.Is(err, resolver.ErrNotFound) || errors.Is(err, resolver.ErrKeyNotFound) || strings.Contains(err.Error(), "unable to resolve") || + errors.Is(err, types.ErrStatusNotFound) || errors.Is(err, types.ErrRevoked) || errors.Is(err, types.ErrCredentialNotValidAtTime) || errors.Is(err, types.ErrPresentationNotValidAtTime) { + return "presentation(s) or credential(s) verification failed: " + err.Error() } - + // Default generic message for other errors - return "presentation(s) or contained credential(s) are invalid" + return "presentation(s) or credential(s) verification failed" }