Skip to content

Commit

Permalink
feat(sdk): origin-based trust info for openid4vp (#836)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrii Holovko <[email protected]>
  • Loading branch information
aholovko authored Nov 25, 2024
1 parent 5ea341e commit 3d2b5ec
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 47 deletions.
2 changes: 1 addition & 1 deletion cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
118 changes: 73 additions & 45 deletions pkg/openid4vp/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,38 @@ 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
}
} else {
rawRequestObject = authorizationRequest
}

reqObject, err := verifyRequestObjectAndDecodeClaims(rawRequestObject, signatureVerifier)
reqObject, err := parseRequestObject(authorizationRequestClientID, rawRequestObject, signatureVerifier)
if err != nil {
return nil, walleterror.NewValidationError(
ErrorModule,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand Down
87 changes: 87 additions & 0 deletions pkg/openid4vp/openid4vp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion pkg/openid4vp/request_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand Down

0 comments on commit 3d2b5ec

Please sign in to comment.