Skip to content

Commit

Permalink
feat(sdk): add interaction_details to CI and Vp flows (#814)
Browse files Browse the repository at this point in the history
feat: add interaction_details to CI and Vp flows

Signed-off-by: Misha Sizov <[email protected]>
  • Loading branch information
mishasizov-SK authored Oct 16, 2024
1 parent 1f649e8 commit 4e24ebe
Show file tree
Hide file tree
Showing 16 changed files with 228 additions and 40 deletions.
9 changes: 9 additions & 0 deletions cmd/wallet-sdk-gomobile/openid4ci/acknowledgment.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ func (a *Acknowledgment) Serialize() (string, error) {
return string(data), nil
}

// SetInteractionDetails extends acknowledgment request with serializedInteractionDetails.
func (a *Acknowledgment) SetInteractionDetails(serializedInteractionDetails string) error {
if err := json.Unmarshal([]byte(serializedInteractionDetails), &a.acknowledgment.InteractionDetails); err != nil {
return fmt.Errorf("decode ci ack interaction details: %w", err)
}

return nil
}

// Success acknowledge issuer that client accepts credentials.
func (a *Acknowledgment) Success() error {
return a.acknowledgment.AcknowledgeIssuer(openid4cigoapi.EventStatusCredentialAccepted, &http.Client{})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ type mockIssuerServerHandler struct {
tokenRequestShouldGiveUnmarshallableResponse bool
credentialRequestShouldFail bool
credentialRequestShouldGiveUnmarshallableResponse bool
ackRequestExpectInteractionDetails bool
credentialResponse []byte
headersToCheck *api.Headers
}
Expand Down Expand Up @@ -177,6 +178,13 @@ func (m *mockIssuerServerHandler) ServeHTTP(writer http.ResponseWriter, //nolint
_, err = writer.Write(m.credentialResponse)
}
case "/oidc/ack_endpoint":
var payload map[string]interface{}
err = json.NewDecoder(request.Body).Decode(&payload)
require.NoError(m.t, err)

_, ok := payload["interaction_details"]
require.Equal(m.t, m.ackRequestExpectInteractionDetails, ok)

writer.WriteHeader(http.StatusNoContent)
}

Expand Down Expand Up @@ -258,10 +266,10 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) {
doRequestCredentialTest(t, nil, true)
})
t.Run("Acknowledge reject", func(t *testing.T) {
doRequestCredentialTestExt(t, nil, false, true, "")
doRequestCredentialTestExt(t, nil, false, true, "", false)
})
t.Run("Acknowledge reject with code", func(t *testing.T) {
doRequestCredentialTestExt(t, nil, false, true, "tc_declined")
doRequestCredentialTestExt(t, nil, false, true, "tc_declined", false)
})
})
t.Run("Success with jwk public key", func(t *testing.T) {
Expand Down Expand Up @@ -594,17 +602,18 @@ func doRequestCredentialTest(t *testing.T, additionalHeaders *api.Headers,
disableTLSVerification bool,
) {
t.Helper()
doRequestCredentialTestExt(t, additionalHeaders, disableTLSVerification, false, "")
doRequestCredentialTestExt(t, additionalHeaders, disableTLSVerification, false, "", true)
}

//nolint:thelper // Not a test helper function
func doRequestCredentialTestExt(t *testing.T, additionalHeaders *api.Headers,
disableTLSVerification bool, acknowledgeReject bool, rejectCode string,
disableTLSVerification bool, acknowledgeReject bool, rejectCode string, expectAckInteractionDetails bool,
) {
issuerServerHandler := &mockIssuerServerHandler{
t: t,
credentialResponse: sampleCredentialResponse,
headersToCheck: additionalHeaders,
t: t,
credentialResponse: sampleCredentialResponse,
headersToCheck: additionalHeaders,
ackRequestExpectInteractionDetails: expectAckInteractionDetails,
}

server := httptest.NewServer(issuerServerHandler)
Expand Down Expand Up @@ -656,6 +665,11 @@ func doRequestCredentialTestExt(t *testing.T, additionalHeaders *api.Headers,
require.NotEmpty(t, acknowledgmentRestored)
require.NoError(t, err)

if expectAckInteractionDetails {
err = acknowledgmentRestored.SetInteractionDetails(`{"key1": "value1"}`)
require.NoError(t, err)
}

if acknowledgeReject {
if rejectCode != "" {
err = acknowledgmentRestored.RejectWithCode(rejectCode)
Expand Down
9 changes: 9 additions & 0 deletions cmd/wallet-sdk-gomobile/openid4vp/acknowledgment.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ func (a *Acknowledgment) Serialize() (string, error) {
return string(data), nil
}

// SetInteractionDetails extends acknowledgment request with serializedInteractionDetails.
func (a *Acknowledgment) SetInteractionDetails(serializedInteractionDetails string) error {
if err := json.Unmarshal([]byte(serializedInteractionDetails), &a.acknowledgment.InteractionDetails); err != nil {
return fmt.Errorf("decode vp ack interaction details: %w", err)
}

return nil
}

// NoConsent acknowledge verifier that user does not consent to the presentation request.
func (a *Acknowledgment) NoConsent() error {
return a.acknowledgment.AcknowledgeVerifier(openid4vp.AccessDeniedErrorResponse,
Expand Down
21 changes: 16 additions & 5 deletions cmd/wallet-sdk-gomobile/openid4vp/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,24 @@ func (o *Interaction) PresentCredentialOpts(

var presentOpts []openid4vp.PresentOpt

if opts != nil && opts.attestationVM != nil {
attestationSigner, attErr := common.NewJWSSigner(opts.attestationVM.ToSDKVerificationMethod(), o.crypto)
if attErr != nil {
return wrapper.ToMobileErrorWithTrace(attErr, o.oTel)
if opts != nil {
if len(opts.serializedInteractionDetails) > 0 {
var interactionDetails map[string]interface{}
if err = json.Unmarshal([]byte(opts.serializedInteractionDetails), &interactionDetails); err != nil {
return fmt.Errorf("decode vp interaction details: %w", err)
}

presentOpts = append(presentOpts, openid4vp.WithInteractionDetails(interactionDetails))
}

presentOpts = append(presentOpts, openid4vp.WithAttestationVC(attestationSigner, opts.attestationVC))
if opts.attestationVM != nil {
attestationSigner, attErr := common.NewJWSSigner(opts.attestationVM.ToSDKVerificationMethod(), o.crypto)
if attErr != nil {
return wrapper.ToMobileErrorWithTrace(attErr, o.oTel)
}

presentOpts = append(presentOpts, openid4vp.WithAttestationVC(attestationSigner, opts.attestationVC))
}
}

return wrapper.ToMobileErrorWithTrace(o.goAPIOpenID4VP.PresentCredential(vcs, claims, presentOpts...), o.oTel)
Expand Down
17 changes: 15 additions & 2 deletions cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ func TestOpenID4VP_PresentCredential(t *testing.T) {

err := instance.PresentCredentialOpts(credentials, NewPresentCredentialOpts().
AddScopeClaim("claim1", `{"key" : "val"}`).
SetAttestationVC(verificationMethod, "invalidVC"))
SetAttestationVC(verificationMethod, "invalidVC").
SetInteractionDetails(`{"key1": "value1"}`))
require.NoError(t, err)
})

Expand Down Expand Up @@ -243,6 +244,14 @@ func TestOpenID4VP_PresentCredential(t *testing.T) {
require.ErrorContains(t, err, `fail to parse "claim1" claim json`)
})

t.Run("Present credentials with invalid interaction details", func(t *testing.T) {
instance := makeInteraction()

err := instance.PresentCredentialOpts(credentials, NewPresentCredentialOpts().
SetInteractionDetails(`"key1": "value1"`))
require.ErrorContains(t, err, `decode vp interaction details`)
})

t.Run("Present credentials unsafe failed", func(t *testing.T) {
instance := makeInteraction()

Expand Down Expand Up @@ -382,13 +391,17 @@ func TestInteraction_Acknowledgment(t *testing.T) {
require.Equal(t, "https://verifier/present", ack.acknowledgment.ResponseURI)
require.Equal(t, "98822a39-9178-4742-a2dc-aba49879fc7b", ack.acknowledgment.State)

err := ack.SetInteractionDetails(`{"key1": "value1"}`)
require.NoError(t, err)

serialized, err := ack.Serialize()
require.NoError(t, err)

ackRestored, err := NewAcknowledgment(serialized)
require.NoError(t, err)
require.Equal(t, ack.acknowledgment.ResponseURI, ackRestored.acknowledgment.ResponseURI)
require.Equal(t, ack.acknowledgment.State, ackRestored.acknowledgment.State)
require.Equal(t, map[string]interface{}{"key1": "value1"}, ackRestored.acknowledgment.InteractionDetails)
})
}

Expand Down Expand Up @@ -424,7 +437,7 @@ func (c *mockCrypto) Sign(_ []byte, _ string) ([]byte, error) {
return c.SignResult, c.SignErr
}

func (c *mockCrypto) Verify([]byte, []byte, string) error {
func (c *mockCrypto) Verify(_ []byte, _ []byte, _ string) error {
return c.VerifyErr
}

Expand Down
14 changes: 12 additions & 2 deletions cmd/wallet-sdk-gomobile/openid4vp/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ func NewPresentCredentialOpts() *PresentCredentialOpts {
type PresentCredentialOpts struct {
scopeClaims map[string]string

attestationVM *api.VerificationMethod
attestationVC string
attestationVM *api.VerificationMethod
attestationVC string
serializedInteractionDetails string
}

// AddScopeClaim adds scope claim with given name.
Expand All @@ -135,3 +136,12 @@ func (o *PresentCredentialOpts) SetAttestationVC(

return o
}

// SetInteractionDetails extends authorization response with interaction details.
func (o *PresentCredentialOpts) SetInteractionDetails(
serializedInteractionDetails string,
) *PresentCredentialOpts {
o.serializedInteractionDetails = serializedInteractionDetails

return o
}
13 changes: 8 additions & 5 deletions pkg/openid4ci/acknowledgment.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ import (

// Acknowledgment represents an object that allows to acknowledge the issuer the user's accepted or rejected credential.
type Acknowledgment struct {
AckIDs []string `json:"ack_ids,omitempty"`
CredentialAckEndpoint string `json:"credential_ack_endpoint,omitempty"`
IssuerURI string `json:"issuer_uri,omitempty"`
AuthToken *universalAuthToken `json:"auth_token,omitempty"`
AckIDs []string `json:"ack_ids,omitempty"`
CredentialAckEndpoint string `json:"credential_ack_endpoint,omitempty"`
IssuerURI string `json:"issuer_uri,omitempty"`
AuthToken *universalAuthToken `json:"auth_token,omitempty"`
InteractionDetails map[string]interface{} `json:"interaction_details,omitempty"`
}

// AcknowledgeIssuer acknowledge issuer that client accepts or rejects credentials.
func (a *Acknowledgment) AcknowledgeIssuer(
eventStatus EventStatus, httpClient *http.Client,
) error {
var ackRequest acknowledgementRequest
ackRequest := acknowledgementRequest{
InteractionDetails: a.InteractionDetails,
}

for _, ackID := range a.AckIDs {
ackRequest.Credentials = append(ackRequest.Credentials, credentialAcknowledgement{
Expand Down
15 changes: 13 additions & 2 deletions pkg/openid4ci/issuerinitiatedinteraction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type mockIssuerServerHandler struct {
batchCredentialResponse []byte
httpStatusCode int
ackRequestErrorResponse string
ackRequestExpectInteractionDetails bool
}

//nolint:gocyclo // test file
Expand Down Expand Up @@ -162,6 +163,13 @@ func (m *mockIssuerServerHandler) ServeHTTP(writer http.ResponseWriter, request
statusCode = m.httpStatusCode
}

var payload map[string]interface{}
err = json.NewDecoder(request.Body).Decode(&payload)
require.NoError(m.t, err)

_, ok := payload["interaction_details"]
require.Equal(m.t, m.ackRequestExpectInteractionDetails, ok)

if m.ackRequestErrorResponse != "" {
_, err = writer.Write([]byte(m.ackRequestErrorResponse))
}
Expand Down Expand Up @@ -684,8 +692,9 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) {
}

issuerServerHandler := &mockIssuerServerHandler{
t: t,
credentialResponse: sampleCredentialResponseAsk,
t: t,
credentialResponse: sampleCredentialResponseAsk,
ackRequestExpectInteractionDetails: true,
}

server := httptest.NewServer(issuerServerHandler)
Expand All @@ -710,6 +719,8 @@ func TestIssuerInitiatedInteraction_RequestCredential(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, requestedAcknowledgment)

requestedAcknowledgment.InteractionDetails = map[string]interface{}{"key1": "value1"}

if !tc.reject {
err = requestedAcknowledgment.AcknowledgeIssuer(openid4ci.EventStatusCredentialAccepted, &http.Client{})
} else {
Expand Down
3 changes: 2 additions & 1 deletion pkg/openid4ci/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ type errorResponse struct {
}

type acknowledgementRequest struct {
Credentials []credentialAcknowledgement `json:"credentials"`
Credentials []credentialAcknowledgement `json:"credentials"`
InteractionDetails map[string]interface{} `json:"interaction_details,omitempty"`
}

type credentialAcknowledgement struct {
Expand Down
16 changes: 14 additions & 2 deletions pkg/openid4vp/acknowledgment.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ package openid4vp
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
Expand All @@ -26,8 +28,9 @@ const (

// Acknowledgment holds data needed to acknowledge the verifier.
type Acknowledgment struct {
ResponseURI string `json:"response_uri"`
State string `json:"state"`
ResponseURI string `json:"response_uri"`
State string `json:"state"`
InteractionDetails map[string]interface{} `json:"interaction_details,omitempty"`
}

// AcknowledgeVerifier sends acknowledgment to the verifier.
Expand All @@ -38,6 +41,15 @@ func (a *Acknowledgment) AcknowledgeVerifier(error, desc string, httpClient http
v.Set("error_description", desc)
v.Set("state", a.State)

if a.InteractionDetails != nil {
interactionDetailsBytes, e := json.Marshal(a.InteractionDetails)
if e != nil {
return fmt.Errorf("encode interaction details: %w", e)
}

v.Add("interaction_details", base64.StdEncoding.EncodeToString(interactionDetailsBytes))
}

req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, a.ResponseURI,
bytes.NewBufferString(v.Encode()))
if err != nil {
Expand Down
21 changes: 21 additions & 0 deletions pkg/openid4vp/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package openid4vp
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/gob"
"encoding/json"
"errors"
Expand Down Expand Up @@ -197,6 +198,8 @@ type presentOpts struct {

attestationVPSigner api.JWTSigner
attestationVC string

interactionDetails map[string]interface{}
}

// PresentOpt is an option for the RequestCredentialWithPreAuth method.
Expand All @@ -213,6 +216,15 @@ func WithAttestationVC(
}
}

// WithInteractionDetails extends authorization response with interaction details.
func WithInteractionDetails(
interactionDetails map[string]interface{},
) PresentOpt {
return func(opts *presentOpts) {
opts.interactionDetails = interactionDetails
}
}

// PresentCredential presents credentials to redirect uri from request object.
func (o *Interaction) PresentCredential(
credentials []*verifiable.Credential,
Expand Down Expand Up @@ -277,6 +289,15 @@ func (o *Interaction) presentCredentials(
data.Set("presentation_submission", response.PresentationSubmission)
data.Set("state", response.State)

if opts.interactionDetails != nil {
interactionDetailsBytes, e := json.Marshal(opts.interactionDetails)
if e != nil {
return fmt.Errorf("encode interaction details: %w", e)
}

data.Add("interaction_details", base64.StdEncoding.EncodeToString(interactionDetailsBytes))
}

err = o.sendAuthorizedResponse(data.Encode())
if err != nil {
return fmt.Errorf("send authorized response failed: %w", err)
Expand Down
Loading

0 comments on commit 4e24ebe

Please sign in to comment.