Skip to content
Open
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
23 changes: 22 additions & 1 deletion auth/api/iam/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import (
"encoding/json"
"errors"
"fmt"

"github.com/nuts-foundation/nuts-node/http/user"
"github.com/nuts-foundation/nuts-node/vcr/types"
"github.com/nuts-foundation/nuts-node/vcr/verifier"

"net/http"
"net/url"
"slices"
Expand Down Expand Up @@ -536,7 +540,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,
}
Expand Down Expand Up @@ -764,3 +768,20 @@ 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 {
// Check for DID resolution errors
if errors.Is(err, verifier.VerificationError{}) {
return 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 verifier.ToVerificationError(err).Error()
}

// Default generic message for other errors
return "presentation(s) or credential(s) verification failed"
}
42 changes: 40 additions & 2 deletions auth/api/iam/openid4vp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ package iam
import (
"context"
"encoding/json"
"github.com/nuts-foundation/nuts-node/http/user"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"testing"

"github.com/nuts-foundation/nuts-node/http/user"
"github.com/nuts-foundation/nuts-node/vcr/verifier"

"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
Expand All @@ -35,6 +39,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"
Expand Down Expand Up @@ -461,7 +466,7 @@ func TestWrapper_HandleAuthorizeResponse(t *testing.T) {

_, err := ctx.client.HandleAuthorizeResponse(context.Background(), baseRequest())

oauthErr := assertOAuthError(t, err, "presentation(s) or contained credential(s) are invalid")
oauthErr := assertOAuthError(t, err, "presentation(s) or credential(s) verification failed")
assert.Equal(t, "https://example.com/iam/holder/cb", oauthErr.RedirectURI.String())
})
t.Run("expired session", func(t *testing.T) {
Expand Down Expand Up @@ -964,5 +969,38 @@ 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 credential(s) verification failed: 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 credential(s) verification failed: 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 credential(s) verification failed: 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 credential(s) verification failed", result)
})

t.Run("VerificationError", func(t *testing.T) {
err := verifier.VerificationError{
Msg: "some verification error",
}
result := verificationErrorDescription(err)
assert.Equal(t, "presentation(s) or credential(s) verification failed: some verification error", result)
})
}
2 changes: 1 addition & 1 deletion auth/api/iam/s2s_vptoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down
2 changes: 1 addition & 1 deletion auth/api/iam/s2s_vptoken_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func TestWrapper_handleS2SAccessTokenRequest(t *testing.T) {

resp, err := ctx.client.handleS2SAccessTokenRequest(contextWithValue, clientID, issuerSubjectID, requestedScope, submissionJSON, presentation.Raw())

assert.EqualError(t, err, "invalid_request - invalid - presentation(s) or contained credential(s) are invalid")
assert.EqualError(t, err, "invalid_request - invalid - presentation(s) or credential(s) verification failed")
assert.Nil(t, resp)
})
t.Run("proof of ownership", func(t *testing.T) {
Expand Down
9 changes: 5 additions & 4 deletions auth/services/selfsigned/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import (
"context"
"encoding/json"
"errors"
"os"
"testing"
"time"

"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/auth/services"
"github.com/nuts-foundation/nuts-node/auth/services/selfsigned/types"
Expand All @@ -33,9 +37,6 @@ import (
"github.com/nuts-foundation/nuts-node/vcr/verifier"
"github.com/nuts-foundation/nuts-node/vdr/didnuts/didstore"
"go.uber.org/mock/gomock"
"os"
"testing"
"time"

ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
Expand Down Expand Up @@ -176,7 +177,7 @@ func TestValidator_VerifyVP(t *testing.T) {

require.NoError(t, err)
assert.Equal(t, contract.Invalid, result.Validity())
assert.Equal(t, "verification error: ", result.Reason())
assert.Equal(t, "presentation(s) or credential(s) verification failed", result.Reason())
})

t.Run("ok using in-memory DBs", func(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions vcr/api/vcr/v2/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1160,15 +1160,15 @@ func TestWrapper_VerifyVP(t *testing.T) {
testContext := newMockContext(t)
request := VPVerificationRequest{VerifiablePresentation: vp}
testContext.mockVerifier.EXPECT().VerifyVP(vp, true, false, nil).Return(nil, verifier.VerificationError{})
errMsg := "verification error: "
expectedRepsonse := VerifyVP200JSONResponse(VPVerificationResult{
errMsg := "presentation(s) or credential(s) verification failed"
expectedResponse := VerifyVP200JSONResponse(VPVerificationResult{
Message: &errMsg,
Validity: false,
})

response, err := testContext.client.VerifyVP(testContext.requestCtx, VerifyVPRequestObject{Body: &request})

assert.Equal(t, expectedRepsonse, response)
assert.Equal(t, expectedResponse, response)
assert.NoError(t, err)
})
}
Expand Down
12 changes: 8 additions & 4 deletions vcr/verifier/signature_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (sv *signatureVerifier) VerifySignature(credentialToVerify vc.VerifiableCre
func (sv *signatureVerifier) VerifyVPSignature(presentation vc.VerifiablePresentation, validateAt *time.Time) error {
signerDID, err := credential.PresentationSigner(presentation)
if err != nil {
return toVerificationError(err)
return ToVerificationError(err)
}

switch presentation.Format() {
Expand Down Expand Up @@ -100,7 +100,7 @@ func (sv *signatureVerifier) jsonldProof(documentToVerify any, issuer string, at
validAt = *at
}
if !ldProof.ValidAt(validAt, maxSkew) {
return toVerificationError(types.ErrPresentationNotValidAtTime)
return ToVerificationError(types.ErrPresentationNotValidAtTime)
}

// find key
Expand Down Expand Up @@ -132,15 +132,19 @@ func (sv *signatureVerifier) jwtSignature(jwtDocumentToVerify string, issuer str
return nil, err
}
metadata.JwtProtectedHeaders = headers
return sv.resolveSigningKey(kid, issuer, metadata)
key, err := sv.resolveSigningKey(kid, issuer, metadata)
if err != nil {
return nil, fmt.Errorf("unable to resolve signing key: %w", err)
}
return key, err
}, jwt.WithClock(jwt.ClockFunc(func() time.Time {
if at == nil {
return time.Now()
}
return *at
})))
if err != nil {
return fmt.Errorf("unable to validate JWT signature: %w", err)
return newVerificationError("unable to validate JWT signature: %w", err)
}
if keyID != "" && strings.Split(keyID, "#")[0] != issuer {
return errVerificationMethodNotOfIssuer
Expand Down
17 changes: 9 additions & 8 deletions vcr/verifier/signature_verifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"os"
"testing"
"time"

"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/cert"
"github.com/lestrrat-go/jwx/v2/jwa"
testpki "github.com/nuts-foundation/nuts-node/test/pki"
"github.com/nuts-foundation/nuts-node/vdr/didx509"
"os"
"testing"
"time"

"github.com/lestrrat-go/jwx/v2/jwk"
ssi "github.com/nuts-foundation/go-did"
Expand Down Expand Up @@ -111,7 +112,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) {
}
sv := x509VerifierTestSetup(t)
err = sv.VerifySignature(*cred, nil)
assert.ErrorIs(t, err, expectedError)
assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: failing ExtractProtectedHeaders")
})
})
t.Run("JWT", func(t *testing.T) {
Expand Down Expand Up @@ -160,7 +161,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) {

err = sv.VerifySignature(*cred, nil)

assert.EqualError(t, err, "unable to validate JWT signature: could not verify message using any of the signatures or keys")
assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: could not verify message using any of the signatures or keys")
})
t.Run("expired token", func(t *testing.T) {
// Credential taken from Sphereon Wallet, expires on Tue Oct 03 2023
Expand All @@ -174,7 +175,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) {
}
err := sv.VerifySignature(*cred, nil)

assert.EqualError(t, err, "unable to validate JWT signature: \"exp\" not satisfied")
assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: \"exp\" not satisfied")
})
t.Run("without kid header, derived from issuer", func(t *testing.T) {
// Credential taken from Sphereon Wallet
Expand Down Expand Up @@ -203,7 +204,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) {
}
err := sv.VerifySignature(*cred, nil)

assert.EqualError(t, err, "unable to validate JWT signature: could not verify message using any of the signatures or keys")
assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: could not verify message using any of the signatures or keys")
})
})

Expand Down Expand Up @@ -257,7 +258,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) {

err := sv.VerifySignature(vc2, nil)

assert.EqualError(t, err, "verification error: missing proof")
assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: missing proof")
})

t.Run("error - wrong jws in proof", func(t *testing.T) {
Expand Down
23 changes: 14 additions & 9 deletions vcr/verifier/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/pki"
"github.com/nuts-foundation/nuts-node/vcr/revocation"
"strings"
"time"

"github.com/nuts-foundation/nuts-node/pki"
"github.com/nuts-foundation/nuts-node/vcr/revocation"

ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
Expand All @@ -44,7 +45,7 @@ const (
maxSkew = 5 * time.Second
)

var errVerificationMethodNotOfIssuer = errors.New("verification method is not of issuer")
var errVerificationMethodNotOfIssuer = newVerificationError("verification method is not of issuer")

// verifier implements the Verifier interface.
// It implements the generic methods for verifying verifiable credentials and verifiable presentations.
Expand All @@ -62,8 +63,8 @@ type verifier struct {

// VerificationError is used to describe a VC/VP verification failure.
type VerificationError struct {
msg string
args []interface{}
Msg string
Args []interface{}
}

// Is checks whether the given error is a VerificationError as well.
Expand All @@ -73,15 +74,19 @@ func (e VerificationError) Is(other error) bool {
}

func newVerificationError(msg string, args ...interface{}) error {
return VerificationError{msg: msg, args: args}
return VerificationError{Msg: msg, Args: args}
}

func toVerificationError(cause error) error {
return VerificationError{msg: cause.Error()}
func ToVerificationError(cause error) error {
return VerificationError{Msg: cause.Error()}
}

func (e VerificationError) Error() string {
return fmt.Errorf("verification error: "+e.msg, e.args...).Error()
const msg = "presentation(s) or credential(s) verification failed"
if e.Msg == "" {
return msg
}
return fmt.Errorf(msg+": "+e.Msg, e.Args...).Error()
}

// NewVerifier creates a new instance of the verifier. It needs a key resolver for validating signatures.
Expand Down
Loading
Loading