From c8f82c7945b40d1a4d782e4304ca314c9d77950a Mon Sep 17 00:00:00 2001 From: Andrii Holovko Date: Fri, 22 Nov 2024 17:37:19 +0200 Subject: [PATCH] feat(sdk): origin-based trust info for openid4vp Signed-off-by: Andrii Holovko --- .../openid4vp/interaction_test.go | 2 +- pkg/openid4vp/openid4vp.go | 118 +++++++++++------- pkg/openid4vp/openid4vp_test.go | 87 +++++++++++++ pkg/openid4vp/request_object.go | 9 +- 4 files changed, 169 insertions(+), 47 deletions(-) diff --git a/cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go b/cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go index 40257a1a..0019c55e 100644 --- a/cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go +++ b/cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go @@ -134,7 +134,7 @@ func TestNewInteraction(t *testing.T) { instance, err := NewInteraction(requiredArgs, nil) testutil.RequireErrorContains(t, err, "INVALID_AUTHORIZATION_REQUEST") - testutil.RequireErrorContains(t, err, "verify request object: parse JWT: invalid public key id:"+ + testutil.RequireErrorContains(t, err, "verify request object: check proof: invalid public key id:"+ " resolve DID did:ion:EiDYWcDuP-EDjVyFWGFdpgPncar9A7OGFykdeX71ZTU-wg:") require.Nil(t, instance) }) diff --git a/pkg/openid4vp/openid4vp.go b/pkg/openid4vp/openid4vp.go index cee3de16..f8f2a8b6 100644 --- a/pkg/openid4vp/openid4vp.go +++ b/pkg/openid4vp/openid4vp.go @@ -105,12 +105,30 @@ func NewInteraction( ) (*Interaction, error) { client, activityLogger, metricsLogger, signer := processOpts(opts) - var rawRequestObject string + var ( + authorizationRequestClientID string + rawRequestObject string + ) + + if strings.HasPrefix(authorizationRequest, "openid-vc://") || + strings.HasPrefix(authorizationRequest, "openid4vp://") { + var ( + authorizationRequestURL *url.URL + err error + ) + + authorizationRequestURL, err = url.Parse(authorizationRequest) + if err != nil { + return nil, walleterror.NewValidationError( + ErrorModule, + InvalidAuthorizationRequestErrorCode, + InvalidAuthorizationRequestError, + err) + } - if strings.HasPrefix(authorizationRequest, "openid-vc://") { - var err error + authorizationRequestClientID = authorizationRequestURL.Query().Get("client_id") - rawRequestObject, err = fetchRequestObject(authorizationRequest, client, metricsLogger) + rawRequestObject, err = fetchRequestObject(authorizationRequestURL, client, metricsLogger) if err != nil { return nil, err } @@ -118,7 +136,7 @@ func NewInteraction( rawRequestObject = authorizationRequest } - reqObject, err := verifyRequestObjectAndDecodeClaims(rawRequestObject, signatureVerifier) + reqObject, err := parseRequestObject(authorizationRequestClientID, rawRequestObject, signatureVerifier) if err != nil { return nil, walleterror.NewValidationError( ErrorModule, @@ -169,21 +187,32 @@ func (o *Interaction) VerifierDisplayData() *VerifierDisplayData { // TrustInfo return verifier trust info. func (o *Interaction) TrustInfo() (*VerifierTrustInfo, error) { - // Verifier is issuer of request object. - verifier := o.requestObject.Issuer + trustInfo := &VerifierTrustInfo{} - verifierDID := strings.Split(verifier, "#")[0] + if o.requestObject.ClientIDScheme == redirectURIScheme { + verifierURI, err := url.Parse(o.requestObject.ResponseURI) + if err != nil { + return nil, err + } - valid, linkedDomain, err := wellknown.ValidateLinkedDomains(verifierDID, o.didResolver, o.httpClient) - if err != nil { - return nil, err + trustInfo.Domain = verifierURI.Host + } else { + // Verifier is issuer of request object. + verifier := o.requestObject.Issuer + + verifierDID := strings.Split(verifier, "#")[0] + + valid, linkedDomain, err := wellknown.ValidateLinkedDomains(verifierDID, o.didResolver, o.httpClient) + if err != nil { + return nil, err + } + + trustInfo.DID = verifierDID + trustInfo.Domain = linkedDomain + trustInfo.DomainValid = valid } - return &VerifierTrustInfo{ - DID: verifierDID, - Domain: linkedDomain, - DomainValid: valid, - }, nil + return trustInfo, nil } // Acknowledgment returns acknowledgment object for the current interaction. @@ -366,18 +395,9 @@ func (o *Interaction) sendAuthorizedResponse(responseBody string) error { return err } -func fetchRequestObject(authorizationRequest string, client httpClient, +func fetchRequestObject(authorizationRequestURL *url.URL, client httpClient, metricsLogger api.MetricsLogger, ) (string, error) { - authorizationRequestURL, err := url.Parse(authorizationRequest) - if err != nil { - return "", walleterror.NewValidationError( - ErrorModule, - InvalidAuthorizationRequestErrorCode, - InvalidAuthorizationRequestError, - err) - } - if !authorizationRequestURL.Query().Has("request_uri") { return "", walleterror.NewValidationError( ErrorModule, @@ -401,15 +421,39 @@ func fetchRequestObject(authorizationRequest string, client httpClient, return string(respBytes), nil } -func verifyRequestObjectAndDecodeClaims( +func parseRequestObject( + authorizationRequestClientID string, rawRequestObject string, signatureVerifier jwt.ProofChecker, ) (*requestObject, error) { reqObject := &requestObject{} - err := verifyTokenSignatureAndDecodeClaims(rawRequestObject, reqObject, signatureVerifier) + _, _, err := jwt.Parse(rawRequestObject, + jwt.DecodeClaimsTo(reqObject), + jwt.WithIgnoreClaimsMapDecoding(true), + ) if err != nil { - return nil, err + return nil, fmt.Errorf("parse jwt: %w", err) + } + + switch reqObject.ClientIDScheme { + case "": //TODO: For backward compatibility, remove this case in the future + fallthrough + case didScheme: + if reqObject.Issuer == "" { + return nil, errors.New("iss claim in request object is required") + } + + err = jwt.CheckProof(rawRequestObject, signatureVerifier, &reqObject.Issuer, nil) + if err != nil { + return nil, fmt.Errorf("check proof: %w", err) + } + case redirectURIScheme: + if !strings.EqualFold(authorizationRequestClientID, reqObject.ResponseURI) { + return nil, errors.New("client_id mismatch between authorization request and request object") + } + default: + return nil, fmt.Errorf("unsupported client_id_scheme: %s", reqObject.ClientIDScheme) } // temporary solution for backward compatibility @@ -430,22 +474,6 @@ func verifyRequestObjectAndDecodeClaims( return reqObject, nil } -func verifyTokenSignatureAndDecodeClaims(rawJwt string, claims interface{}, proofChecker jwt.ProofChecker) error { - jsonWebToken, _, err := jwt.ParseAndCheckProof(rawJwt, proofChecker, true, - jwt.DecodeClaimsTo(claims), - jwt.WithIgnoreClaimsMapDecoding(true)) - if err != nil { - return fmt.Errorf("parse JWT: %w", err) - } - - err = jsonWebToken.DecodeClaims(claims) - if err != nil { - return fmt.Errorf("decode claims: %w", err) - } - - return nil -} - func createAuthorizedResponse( credentials []*verifiable.Credential, requestObject *requestObject, diff --git a/pkg/openid4vp/openid4vp_test.go b/pkg/openid4vp/openid4vp_test.go index 22a96c06..c3727742 100644 --- a/pkg/openid4vp/openid4vp_test.go +++ b/pkg/openid4vp/openid4vp_test.go @@ -27,6 +27,7 @@ import ( "github.com/trustbloc/kms-go/doc/jose/jwk" "github.com/trustbloc/kms-go/doc/util/jwkkid" "github.com/trustbloc/kms-go/spi/kms" + "github.com/trustbloc/vc-go/jwt" "github.com/trustbloc/vc-go/presexch" "github.com/trustbloc/vc-go/verifiable" @@ -118,6 +119,60 @@ func TestNewInteraction(t *testing.T) { require.NoError(t, err) require.NotNil(t, interaction) }) + t.Run("openid4vp protocol with redirect_uri client id scheme", func(t *testing.T) { + reqObject := &requestObject{ + ClientIDScheme: redirectURIScheme, + ResponseURI: "https://example.com/redirect", + } + + token, err := jwt.NewUnsecured(reqObject) + require.NoError(t, err) + + reqObjectJWT, err := token.Serialize(false) + require.NoError(t, err) + + interaction, err := NewInteraction("openid4vp://authorize?client_id=https://example.com/redirect&"+ + "request_uri=https://example.com/request-object", + &jwtSignatureVerifierMock{}, + nil, + nil, + nil, + WithHTTPClient(&mock.HTTPClientMock{ + Response: reqObjectJWT, + StatusCode: 200, + ExpectedEndpoint: "https://example.com/request-object", + }), + ) + require.NoError(t, err) + require.NotNil(t, interaction) + }) + t.Run("client_id mismatch between authorization request and request object", func(t *testing.T) { + reqObject := &requestObject{ + ClientIDScheme: redirectURIScheme, + ResponseURI: "https://invalid.example.com/redirect", + } + + token, err := jwt.NewUnsecured(reqObject) + require.NoError(t, err) + + reqObjectJWT, err := token.Serialize(false) + require.NoError(t, err) + + interaction, err := NewInteraction("openid4vp://authorize?client_id=https://example.com/redirect&"+ + "request_uri=https://example.com/request-object", + &jwtSignatureVerifierMock{}, + nil, + nil, + nil, + WithHTTPClient(&mock.HTTPClientMock{ + Response: reqObjectJWT, + StatusCode: 200, + ExpectedEndpoint: "https://example.com/request-object", + }), + ) + require.ErrorContains(t, err, "client_id mismatch between authorization request and request object") + require.Nil(t, interaction) + }) }) t.Run("Fetch Request failed", func(t *testing.T) { @@ -926,6 +981,38 @@ func TestOpenID4VP_TrustInfo(t *testing.T) { require.Equal(t, "mock-uri", info.Domain) }) + t.Run("Success: origin-based trust info", func(t *testing.T) { + reqObject := &requestObject{ + ClientIDScheme: redirectURIScheme, + ResponseURI: "https://example.com/redirect", + } + + token, err := jwt.NewUnsecured(reqObject) + require.NoError(t, err) + + reqObjectJWT, err := token.Serialize(false) + require.NoError(t, err) + + interaction, err := NewInteraction("openid4vp://authorize?client_id=https://example.com/redirect&"+ + "request_uri=https://example.com/request-object", + &jwtSignatureVerifierMock{}, + &didResolverMock{ResolveValue: mockResolutionWithServices(t, mockDID)}, + &cryptoMock{SignVal: []byte(testSignature)}, + lddl, + WithHTTPClient(&mock.HTTPClientMock{ + Response: reqObjectJWT, + StatusCode: 200, + ExpectedEndpoint: "https://example.com/request-object", + }), + ) + require.NoError(t, err) + + info, err := interaction.TrustInfo() + require.NoError(t, err) + require.NotNil(t, info) + require.Equal(t, "example.com", info.Domain) + }) + t.Run("Failure", func(t *testing.T) { httpClient := &mock.HTTPClientMock{ StatusCode: 200, diff --git a/pkg/openid4vp/request_object.go b/pkg/openid4vp/request_object.go index c940f8a7..1f8c0af9 100644 --- a/pkg/openid4vp/request_object.go +++ b/pkg/openid4vp/request_object.go @@ -8,6 +8,13 @@ package openid4vp import "github.com/trustbloc/vc-go/presexch" +type clientIDScheme string + +const ( + didScheme clientIDScheme = "did" + redirectURIScheme clientIDScheme = "redirect_uri" +) + type requestObject struct { JTI string `json:"jti"` IAT int64 `json:"iat"` @@ -18,7 +25,7 @@ type requestObject struct { Scope string `json:"scope"` Nonce string `json:"nonce"` ClientID string `json:"client_id"` //nolint: tagliatelle - ClientIDScheme string `json:"client_id_scheme"` + ClientIDScheme clientIDScheme `json:"client_id_scheme"` State string `json:"state"` Exp int64 `json:"exp"` ClientMetadata clientMetadata `json:"client_metadata"`