Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk): origin-based trust info for openid4vp #836

Merged
Merged
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
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
Loading