Skip to content

Commit

Permalink
feat(sdk): verifier acknowledgment
Browse files Browse the repository at this point in the history
Signed-off-by: Andrii Holovko <[email protected]>
  • Loading branch information
aholovko committed Aug 19, 2024
1 parent 6ad6c1c commit 52f3160
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 3 deletions.
6 changes: 6 additions & 0 deletions cmd/wallet-sdk-gomobile/openid4vp/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type goAPIOpenID4VP interface {
PresentCredentialUnsafe(credential *afgoverifiable.Credential, customClaims openid4vp.CustomClaims) error
VerifierDisplayData() *openid4vp.VerifierDisplayData
TrustInfo() (*openid4vp.VerifierTrustInfo, error)
AckNoConsent() error
}

// VerifierTrustInfo represent verifier trust information.
Expand Down Expand Up @@ -240,6 +241,11 @@ func (o *Interaction) PresentCredentialUnsafe(credential *verifiable.Credential)
openid4vp.CustomClaims{}), o.oTel)
}

// AckNoConsent notifies the verifier that the user did not consent to share credentials.
func (o *Interaction) AckNoConsent() error {
return wrapper.ToMobileErrorWithTrace(o.goAPIOpenID4VP.AckNoConsent(), o.oTel)
}

// OTelTraceID returns open telemetry trace id.
func (o *Interaction) OTelTraceID() string {
traceID := ""
Expand Down
27 changes: 27 additions & 0 deletions cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,28 @@ func TestInteraction_TrustInfo(t *testing.T) {
})
}

func TestInteraction_AckNoConsent(t *testing.T) {
t.Run("Success", func(t *testing.T) {
instance := &Interaction{
goAPIOpenID4VP: &mockGoAPIInteraction{},
}

err := instance.AckNoConsent()
require.NoError(t, err)
})

t.Run("Failure", func(t *testing.T) {
instance := &Interaction{
goAPIOpenID4VP: &mockGoAPIInteraction{
AckNoConsentErr: errors.New("ack no consent err"),
},
}

err := instance.AckNoConsent()
require.ErrorContains(t, err, "ack no consent err")
})
}

type documentLoaderWrapper struct {
goAPIDocumentLoader ld.DocumentLoader
}
Expand Down Expand Up @@ -412,6 +434,7 @@ type mockGoAPIInteraction struct {

PresentedClaimsResult interface{}
PresentedClaimsErr error
AckNoConsentErr error
}

func (o *mockGoAPIInteraction) GetQuery() *presexch.PresentationDefinition {
Expand Down Expand Up @@ -446,6 +469,10 @@ func (o *mockGoAPIInteraction) TrustInfo() (*openid4vp.VerifierTrustInfo, error)
return o.VerifierTrustInfo, o.VerifierTrustInfoErr
}

func (o *mockGoAPIInteraction) AckNoConsent() error {
return o.AckNoConsentErr
}

type mockDIDResolver struct {
ResolveDocBytes []byte
ResolveErr error
Expand Down
15 changes: 15 additions & 0 deletions pkg/openid4vp/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ SPDX-License-Identifier: Apache-2.0

package openid4vp

import "errors"

// Constants' names and reasons are obvious so they do not require additional comments.
// nolint:golint,nolintlint
const (
Expand Down Expand Up @@ -61,3 +63,16 @@ type innerError struct {
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}

// ErrNoCredentialsMatchFound is returned when no credentials match is found.
var ErrNoCredentialsMatchFound = errors.New("no credentials match found")

const (
// AccessDeniedErrorResponse is returned in "error" of Authorization Error Response when no consent is provided or
// no credentials match found.
AccessDeniedErrorResponse = "access_denied"
// NoConsentErrorDescription is returned in "error_description" of Authorization Error Response when no consent is provided.
NoConsentErrorDescription = "no_consent"
// NoMatchFoundErrorDescription is returned in "error_description" of Authorization Error Response when no credentials match found.
NoMatchFoundErrorDescription = "no_match_found"
)
22 changes: 21 additions & 1 deletion pkg/openid4vp/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ func (o *Interaction) presentCredentials(
opts,
)
if err != nil {
if errors.Is(err, ErrNoCredentialsMatchFound) {
if e := o.sendErrorResponse(AccessDeniedErrorResponse, NoMatchFoundErrorDescription); e != nil {
return e
}
}

return walleterror.NewExecutionError(
ErrorModule,
CreateAuthorizedResponseFailedCode,
Expand Down Expand Up @@ -325,6 +331,20 @@ func (o *Interaction) PresentedClaims(credential *verifiable.Credential) (interf
return copyJSONKeysOnly(vcContent.Subject[0].CustomFields), nil
}

// AckNoConsent sends a "no consent" error response to the verifier.
func (o *Interaction) AckNoConsent() error {
return o.sendErrorResponse(AccessDeniedErrorResponse, NoConsentErrorDescription)
}

func (o *Interaction) sendErrorResponse(error, description string) error {
v := url.Values{}
v.Set("error", error)
v.Set("error_description", description)
v.Set("state", o.requestObject.State)

return o.sendAuthorizedResponse(v.Encode())
}

func (o *Interaction) sendAuthorizedResponse(responseBody string) error {
_, err := httprequest.New(o.httpClient, o.metricsLogger).Do(http.MethodPost,
o.requestObject.ResponseURI, "application/x-www-form-urlencoded",
Expand Down Expand Up @@ -426,7 +446,7 @@ func createAuthorizedResponse(
) (*authorizedResponse, error) {
switch len(credentials) {
case 0:
return nil, fmt.Errorf("expected at least one credential to present to verifier")
return nil, ErrNoCredentialsMatchFound
case 1:
return createAuthorizedResponseOneCred(credentials[0], requestObject, customClaims,
didResolver, crypto, documentLoader, opts)
Expand Down
68 changes: 67 additions & 1 deletion pkg/openid4vp/openid4vp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,28 @@ func TestOpenID4VP_PresentCredential(t *testing.T) {

err = interaction.PresentCredential(nil, CustomClaims{})
require.Error(t, err)
require.Contains(t, err.Error(), "expected at least one credential")
require.Contains(t, err.Error(), "no credentials match found")
})

t.Run("fail to send error response when no credentials match found", func(t *testing.T) {
interaction, err := NewInteraction(
requestObjectJWT,
&jwtSignatureVerifierMock{},
&didResolverMock{ResolveValue: mockDoc},
&cryptoMock{SignVal: []byte(testSignature)},
lddl,
WithHTTPClient(&mock.HTTPClientMock{
Err: errors.New("http error"),
}),
)
require.NoError(t, err)

query := interaction.GetQuery()
require.NotNil(t, query)

err = interaction.PresentCredential(nil, CustomClaims{})
require.Error(t, err)
require.Contains(t, err.Error(), "http error")
})

t.Run("no subject ID found", func(t *testing.T) {
Expand Down Expand Up @@ -829,6 +850,51 @@ func TestOpenID4VP_PresentCredential(t *testing.T) {
})
}

func TestOPenID4VP_AckNoConsent(t *testing.T) {
lddl := testutil.DocumentLoader(t)
mockDoc := mockResolution(t, mockDID)

t.Run("Success", func(t *testing.T) {
client := &mock.HTTPClientMock{
StatusCode: 200,
}

interaction, err := NewInteraction(
requestObjectJWT,
&jwtSignatureVerifierMock{},
&didResolverMock{ResolveValue: mockDoc},
&cryptoMock{SignVal: []byte(testSignature)},
lddl,
WithHTTPClient(client),
)
require.NoError(t, err)

err = interaction.AckNoConsent()

require.NoError(t, err)
})

t.Run("Fail to send error response", func(t *testing.T) {
client := &mock.HTTPClientMock{
Err: errors.New("http error"),
}

interaction, err := NewInteraction(
requestObjectJWT,
&jwtSignatureVerifierMock{},
&didResolverMock{ResolveValue: mockDoc},
&cryptoMock{SignVal: []byte(testSignature)},
lddl,
WithHTTPClient(client),
)
require.NoError(t, err)

err = interaction.AckNoConsent()

require.ErrorContains(t, err, "http error")
})
}

func TestOpenID4VP_TrustInfo(t *testing.T) {
lddl := testutil.DocumentLoader(t)

Expand Down
2 changes: 1 addition & 1 deletion test/integration/fixtures/.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# vc services
VC_REST_IMAGE=ghcr.io/trustbloc-cicd/vc-server
VC_REST_IMAGE_TAG=v1.8.2-snapshot-49a9df8
VC_REST_IMAGE_TAG=v1.10.1-snapshot-c293c0a

# Remote JSON-LD context provider
CONTEXT_PROVIDER_URL=https://file-server.trustbloc.local:10096/ld-contexts.json
Expand Down
13 changes: 13 additions & 0 deletions test/integration/openid4vp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func TestOpenID4VPFullFlow(t *testing.T) {
customScopes []customScope
trustInfo bool
shouldBeForbidden bool
ackNoConsent bool
}

tests := []test{
Expand Down Expand Up @@ -170,6 +171,13 @@ func TestOpenID4VPFullFlow(t *testing.T) {
},
},
},
{
issuerProfileIDs: []string{"bank_issuer"},
claimData: []claimData{verifiableEmployeeClaims},
walletDIDMethod: "ion",
verifierProfileID: "v_myprofile_jwt_verified_employee",
ackNoConsent: true,
},
}

var traceIDs []string
Expand Down Expand Up @@ -266,6 +274,11 @@ func TestOpenID4VPFullFlow(t *testing.T) {
didResolver)
}

if tc.ackNoConsent {
require.NoError(t, interaction.AckNoConsent())
continue
}

selectedCreds := verifiable.NewCredentialsArray()
for ind := 0; ind < matchedVCs.Length(); ind++ {
vcID := matchedVCs.AtIndex(ind).ID()
Expand Down

0 comments on commit 52f3160

Please sign in to comment.