Skip to content
This repository has been archived by the owner on Apr 5, 2023. It is now read-only.

Commit

Permalink
wip: WACI issuance integration with DIDComm v2
Browse files Browse the repository at this point in the history
Signed-off-by: Filip Burlacu <[email protected]>
  • Loading branch information
Filip Burlacu committed Feb 1, 2022
1 parent a6c863d commit 87a1ad0
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 71 deletions.
13 changes: 12 additions & 1 deletion pkg/did/trustbloc_did_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (p *TrustblocDIDCreator) newKey() (crypto.PublicKey, error) {
func (p *TrustblocDIDCreator) templateV2() (*did.Doc, error) {
didDoc := did.Doc{}

auth, err := p.createVerification("#key-1", p.keyType, did.Authentication)
auth, err := p.createVerification( /*"#key-1"*/ "", p.keyType, did.Authentication)
if err != nil {
return nil, fmt.Errorf("creating did doc Authentication: %w", err)
}
Expand All @@ -167,6 +167,13 @@ func (p *TrustblocDIDCreator) templateV2() (*did.Doc, error) {

didDoc.KeyAgreement = append(didDoc.KeyAgreement, *kagr)

assrt, err := p.createVerification( /*"#key-3"*/ "", p.keyType, did.AssertionMethod)
if err != nil {
return nil, fmt.Errorf("creating did doc AssertionMethod: %w", err)
}

didDoc.AssertionMethod = append(didDoc.AssertionMethod, *assrt)

didDoc.Service = []did.Service{{
ID: uuid.NewString(),
ServiceEndpoint: p.didcommInboundURL,
Expand All @@ -192,6 +199,10 @@ func (p *TrustblocDIDCreator) createVerificationMethod(id string, kt kms.KeyType
return nil, fmt.Errorf("creating public key: %w", err)
}

if id == "" {
id = "#" + kid
}

var j *jwk.JWK

if kt == kms.ED25519Type {
Expand Down
2 changes: 2 additions & 0 deletions pkg/profile/issuer/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type ProfileData struct {
LinkedWalletURL string `json:"linkedWallet,omitempty"`
IssuerID string `json:"issuerID,omitempty"`
CMStyle cm.Styles `json:"styles,omitempty"`
PublicDID string `json:"publicDID,omitempty"`
IsDIDCommV2 bool `json:"isDIDCommV2,omitempty"`
}

// OIDCClientParams optional set of oidc client parameters that the issuer may set, for static client registration.
Expand Down
23 changes: 12 additions & 11 deletions pkg/restapi/issuer/operation/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ package operation
import (
"encoding/json"

"github.com/hyperledger/aries-framework-go/pkg/client/outofband"
"github.com/hyperledger/aries-framework-go/pkg/doc/cm"
"github.com/hyperledger/aries-framework-go/pkg/wallet"

adaptervc "github.com/trustbloc/edge-adapter/pkg/vc"
)
Expand All @@ -28,6 +28,7 @@ type ProfileDataRequest struct {
OIDCClientParams *OIDCClientParams `json:"oidcParams,omitempty"`
CredentialScopes []string `json:"credScopes,omitempty"`
LinkedWalletURL string `json:"linkedWallet,omitempty"`
IsDIDCommV2 bool `json:"isDIDCommV2,omitempty"`
// Issuer ID identifies who is the issuer of the credential manifests being issued.
IssuerID string `json:"issuerID,omitempty"`
// CMStyle represents an entity styles object as defined in credential manifest spec.
Expand All @@ -49,20 +50,20 @@ type WalletConnect struct {
// txnData contains session data.
type txnData struct {
// Todo #580 rename IssuerID to ProfileID
IssuerID string `json:"issuerID,omitempty"`
State string `json:"state,omitempty"`
DIDCommInvitation *outofband.Invitation `json:"didCommInvitation,omitempty"`
Token string `json:"token,omitempty"`
CredScope string `json:"cred,omitempty"`
IssuerID string `json:"issuerID,omitempty"`
State string `json:"state,omitempty"`
DIDCommInvitation *wallet.GenericInvitation `json:"didCommInvitation,omitempty"`
Token string `json:"token,omitempty"`
CredScope string `json:"cred,omitempty"`
}

// CredentialHandlerRequest wallet chapi request.
type CredentialHandlerRequest struct {
Query *CHAPIQuery `json:"query,omitempty"`
DIDCommInvitation *outofband.Invitation `json:"invitation,omitempty"`
Credentials []json.RawMessage `json:"credentials,omitempty"`
WACI bool `json:"waci,omitempty"`
WalletRedirect string `json:"walletRedirect,omitempty"`
Query *CHAPIQuery `json:"query,omitempty"`
DIDCommInvitation *wallet.GenericInvitation `json:"invitation,omitempty"`
Credentials []json.RawMessage `json:"credentials,omitempty"`
WACI bool `json:"waci,omitempty"`
WalletRedirect string `json:"walletRedirect,omitempty"`
}

// CHAPIQuery chapi query type data.
Expand Down
111 changes: 97 additions & 14 deletions pkg/restapi/issuer/operation/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/hyperledger/aries-framework-go/pkg/client/issuecredential"
"github.com/hyperledger/aries-framework-go/pkg/client/mediator"
"github.com/hyperledger/aries-framework-go/pkg/client/outofband"
"github.com/hyperledger/aries-framework-go/pkg/client/outofbandv2"
"github.com/hyperledger/aries-framework-go/pkg/client/presentproof"
"github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service"
"github.com/hyperledger/aries-framework-go/pkg/didcomm/messaging/msghandler"
Expand All @@ -41,6 +42,7 @@ import (
"github.com/hyperledger/aries-framework-go/pkg/doc/verifiable"
"github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr"
"github.com/hyperledger/aries-framework-go/pkg/store/connection"
"github.com/hyperledger/aries-framework-go/pkg/wallet"
"github.com/hyperledger/aries-framework-go/spi/storage"
"github.com/piprate/json-gold/ld"
"github.com/trustbloc/edge-core/pkg/log"
Expand Down Expand Up @@ -118,6 +120,7 @@ type connections interface {
// PublicDIDCreator creates public DIDs.
type PublicDIDCreator interface {
Create() (*did.Doc, error)
CreateV2() (*did.Doc, error)
}

type mediatorClientProvider interface {
Expand Down Expand Up @@ -160,6 +163,11 @@ func New(config *Config) (*Operation, error) { // nolint:funlen,gocyclo,cyclop
return nil, fmt.Errorf("failed to create aries outofband client : %w", err)
}

oobv2Client, err := outofbandV2Client(config.AriesCtx)
if err != nil {
return nil, fmt.Errorf("failed to create aries outofband v2 client : %w", err)
}

mediatorClient, err := mediatorClient(config.AriesCtx)
if err != nil {
return nil, fmt.Errorf("failed to create aries mediator client : %w", err)
Expand Down Expand Up @@ -259,6 +267,7 @@ func New(config *Config) (*Operation, error) { // nolint:funlen,gocyclo,cyclop

op := &Operation{
oobClient: oobClient,
oobV2Client: oobv2Client,
didExClient: didExClient,
issueCredClient: issueCredClient,
presentProofClient: presentProofClient,
Expand Down Expand Up @@ -310,6 +319,7 @@ type oidcClient interface {
// Operation defines handlers for rp operations.
type Operation struct {
oobClient *outofband.Client
oobV2Client *outofbandv2.Client
didExClient didExClient
issueCredClient *issuecredential.Client
presentProofClient *presentproof.Client
Expand Down Expand Up @@ -367,7 +377,15 @@ func (o *Operation) createIssuerProfileHandler(rw http.ResponseWriter, req *http
return
}

newDidDoc, err := o.publicDIDCreator.Create()
var newDidDoc *did.Doc
var err error

if data.IsDIDCommV2 {
newDidDoc, err = o.publicDIDCreator.CreateV2()
} else {
newDidDoc, err = o.publicDIDCreator.Create()
}

if err != nil {
commhttp.WriteErrorResponseWithLog(rw, http.StatusInternalServerError,
fmt.Sprintf("failed to create public did : %s", err.Error()), profileEndpoint, logger)
Expand Down Expand Up @@ -1017,10 +1035,40 @@ func (o *Operation) validateAndGetConnection(connectData *issuervc.DIDConnectCre
}

func (o *Operation) createTxn(profile *issuer.ProfileData, state, token, credScope string) (string, error) {
invitation, err := o.oobClient.CreateInvitation(nil, outofband.WithLabel("issuer"),
outofband.WithGoal("", oobGoalCode))
var invBytes []byte

if profile.IsDIDCommV2 {
invitationV2, err := o.oobV2Client.CreateInvitation(
outofbandv2.WithFrom(profile.PublicDID),
outofbandv2.WithLabel("issuer"),
outofbandv2.WithGoal("", oobGoalCode),
)
if err != nil {
return "", fmt.Errorf("failed to create invitation : %w", err)
}

invBytes, err = json.Marshal(invitationV2)
if err != nil {
return "", fmt.Errorf("marshal invitation: %w", err)
}
} else {
invitationV1, err := o.oobClient.CreateInvitation(nil, outofband.WithLabel("issuer"),
outofband.WithGoal("", oobGoalCode))
if err != nil {
return "", fmt.Errorf("failed to create invitation : %w", err)
}

invBytes, err = json.Marshal(invitationV1)
if err != nil {
return "", fmt.Errorf("marshal invitation: %w", err)
}
}

invitation := &wallet.GenericInvitation{}

err := json.Unmarshal(invBytes, invitation)
if err != nil {
return "", fmt.Errorf("failed to create invitation : %w", err)
return "", fmt.Errorf("unmarshal invitation: %w", err)
}

txnID := uuid.New().String()
Expand Down Expand Up @@ -1180,14 +1228,14 @@ func (o *Operation) didCommActionListener(ch <-chan service.DIDCommAction) {
var args interface{}

switch msg.Message.Type() {
case issuecredsvc.RequestCredentialMsgTypeV2:
case issuecredsvc.RequestCredentialMsgTypeV2, issuecredsvc.RequestCredentialMsgTypeV3:
args, err = o.handleRequestCredential(msg)
case presentproofsvc.RequestPresentationMsgTypeV2:
args, err = o.handleRequestPresentation(msg)
case presentproofsvc.RequestPresentationMsgTypeV3:
// TODO handle presentproofsvc.RequestPresentationMsgTypeV3 properly, for now it's the same as V2.
args, err = o.handleRequestPresentation(msg)
case issuecredsvc.ProposeCredentialMsgTypeV2:
case issuecredsvc.ProposeCredentialMsgTypeV2, issuecredsvc.ProposeCredentialMsgTypeV3:
args, err = o.handleProposeCredential(msg)
default:
err = fmt.Errorf("unsupported message type : %s", msg.Message.Type())
Expand Down Expand Up @@ -1221,19 +1269,35 @@ func (o *Operation) didCommStateMsgListener(stateMsgCh <-chan service.StateMsg)

// nolint:funlen,gocyclo,cyclop
func (o *Operation) handleProposeCredential(msg service.DIDCommAction) (issuecredsvc.Opt, error) {
var proposal issuecredential.ProposeCredentialV2
var invitationID string

err := msg.Message.Decode(&proposal)
if err != nil {
return nil, fmt.Errorf("failed to decode propose credential message: %w", err)
// TODO: update afgo to support parsing InvitationID for ProposeCredentialParams
switch msg.Message.Type() {
case issuecredsvc.ProposeCredentialMsgTypeV2:
var proposal issuecredential.ProposeCredentialV2

err := msg.Message.Decode(&proposal)
if err != nil {
return nil, fmt.Errorf("failed to decode propose credential message: %w", err)
}

invitationID = proposal.InvitationID
case issuecredsvc.ProposeCredentialMsgTypeV3:
var proposal issuecredential.ProposeCredentialV3

err := msg.Message.Decode(&proposal)
if err != nil {
return nil, fmt.Errorf("failed to decode propose credential message: %w", err)
}

invitationID = proposal.InvitationID
}

if proposal.InvitationID == "" {
if invitationID == "" {
return nil, errors.New("invalid invitation ID, failed to correlate incoming propose credential message")
}

// TODO: proposecredential v3 uses pthid
userInvMap, err := o.getUserInvitationMapping(proposal.InvitationID)
userInvMap, err := o.getUserInvitationMapping(invitationID)
if err != nil {
return nil, fmt.Errorf("failed to get user invitation mapping : %w", err)
}
Expand Down Expand Up @@ -1292,6 +1356,7 @@ func (o *Operation) handleProposeCredential(msg service.DIDCommAction) (issuecre
offerCred := prepareOfferCredentialMessage(manifest, fulfillment)

// save fulfillment for subsequent WACI steps.
// TODO question: why is this saved under the message ID instead of thread ID?
err = o.saveCredentialFulfillment(msg.Message.ID(), fulfillment)
if err != nil {
return nil, fmt.Errorf("failed to persist credential fulfillment : %w", err)
Expand Down Expand Up @@ -1404,10 +1469,17 @@ func (o *Operation) handleRequestCredential(msg service.DIDCommAction) (interfac

// TODO read credential application from msg nd validate if found [Issue#564]
func (o *Operation) handleWACIRequestCredential(msg service.DIDCommAction, profile *issuer.ProfileData, userConnMap *UserConnectionMapping) (issuecredsvc.Opt, error) { // nolint:lll
thID, err := msg.Message.ThreadID()
var thID string
var err error

// if profile.IsDIDCommV2 {
// thID = msg.Message.ParentThreadID()
// } else {
thID, err = msg.Message.ThreadID()
if err != nil {
return nil, fmt.Errorf("failed to read threadID from request credential message: %w", err)
}
// }

if thID == "" {
return nil, errors.New("failed to correlate WACI interaction, missing thread ID")
Expand Down Expand Up @@ -1796,6 +1868,15 @@ func outofbandClient(ariesCtx outofband.Provider) (*outofband.Client, error) {
return c, nil
}

func outofbandV2Client(ariesCtx outofband.Provider) (*outofbandv2.Client, error) {
c, err := outofbandv2.New(ariesCtx)
if err != nil {
return nil, fmt.Errorf("failed to create new outofband 2.0 client: %w", err)
}

return c, nil
}

func didExchangeClient(ariesCtx aries.CtxProvider, stateMsgCh chan service.StateMsg) (*didexchange.Client, error) {
didExClient, err := didexchange.New(ariesCtx)
if err != nil {
Expand Down Expand Up @@ -2069,5 +2150,7 @@ func mapProfileReqToData(data *ProfileDataRequest, didDoc *did.Doc) (*issuer.Pro
LinkedWalletURL: data.LinkedWalletURL,
IssuerID: data.IssuerID,
CMStyle: data.CMStyle,
PublicDID: didDoc.ID,
IsDIDCommV2: data.IsDIDCommV2,
}, nil
}
19 changes: 17 additions & 2 deletions test/bdd/features/waci_didcommv2.feature
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,26 @@
@waci_didcommv2
Feature: WACI DIDComm V2

Background: Setup External Agent
@issuer_adapter_waci_v2
Scenario Outline: Issuer adapter features
Given "WalletApp" agent is running on "localhost" port "9081" with webhook "http://localhost:9083" and controller "http://localhost:9082"
And Wallet "WalletApp" has profile created and unlocked

Given Issuer Profile with id "<profileID>", name "<profileName>", issuerURL "<issuerURL>", supportedVCContexts "<supportedVCContexts>", credScopes "<credScopes>", issuer id "<issuerID>", linked wallet "<linkedWallet>" and oidc provider "https://issuer-hydra.trustbloc.local:9044/" with DIDComm V2 and WACI support
And Retrieved profile with id "<profileID>" contains name "<profileName>", issuerURL "<issuerURL>", supportedVCContexts "<supportedVCContexts>", credScopes "<credScopes>", issuer id "<issuerID>", linked wallet "<linkedWallet>" and oidc provider "https://issuer-hydra.trustbloc.local:9044/" with DIDComm V2 and WACI support
Then Issuer adapter shows the wallet connect UI when the issuer "<profileID>" with cred scope "<credScopes>" wants to connect to the wallet
And Issuer adapter ("<profileID>") creates DIDComm connection invitation for "<walletID>"
And "<walletID>" accepts invitation from issuer adapter "<profileID>" and performs WACI credential issuance interaction
And "<walletID>" received web redirect info from "<profileID>" after successful completion of WACI credential issuance interaction
Examples:
| profileID | profileName | issuerURL | supportedVCContexts | credScopes | issuerID | linkedWallet | walletID |
| prCardWACI | PRCard Issuer | http://mock-issuer.com:9080/prCard | https://trustbloc.github.io/context/vc/examples/citizenship-v1.jsonld | prc | did:example:123?linked-domains=3 | https://example.wallet.com/waci | WalletApp |

@verifier_adapter_waci_v2
Scenario: WACI flow between Verifier and Wallet using DIDComm V2
Given the "Mock Wallet" is running on "localhost" port "9081" with webhook "http://localhost:9083" and controller "http://localhost:9082"
And the "Mock Issuer Adapter" is running on "localhost" port "10010" with controller "http://localhost:10011"

Scenario: WACI flow between Verifier and Wallet using DIDComm V2
Given a registered rp tenant with label "waci_demo" and scopes "driver_license:local" and linked wallet "https://example.wallet.com/waci" with WACI support using DIDComm V2
When the rp tenant "waci_demo" redirects the user to the rp adapter with scope "driver_license:local"
And the rp adapter "waci_demo" submits a CHAPI request to "Mock Wallet" with out-of-band invitation
Expand Down
2 changes: 1 addition & 1 deletion test/bdd/fixtures/integration/.env
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ USE_DIDCOMM_V2=false

# Aries Agent configurations
AGENT_SDK_REST_IMAGE=ghcr.io/trustbloc-cicd/agent-sdk-server
AGENT_SDK_REST_IMAGE_TAG=0.1.8-snapshot-8553b95
AGENT_SDK_REST_IMAGE_TAG=0.1.8-snapshot-4306979

# Webhook configurations
MOCK_WEBHOOK_IMAGE=ghcr.io/trustbloc/edge-adapter/mock-webhook
Expand Down
4 changes: 2 additions & 2 deletions test/bdd/pkg/agent/agent_controller_steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ func (a *Steps) handleDIDCommConnectRequest(agentID, supportedVCContexts, issuer
return fmt.Errorf("failed to register msg svc: %w", err)
}

connectionID, err := a.AcceptOOBInvitation(agentID, request.DIDCommInvitation, issuerID)
connectionID, err := a.AcceptOOBInvitation(agentID, (*outofband.Invitation)(request.DIDCommInvitation.AsV1()), issuerID)
if err != nil {
return fmt.Errorf("failed to accept oob invitation: %w", err)
}
Expand Down Expand Up @@ -365,7 +365,7 @@ func (a *Steps) didConnectReqWithRouting(agentID, routerURL, issuerID string) er
return fmt.Errorf("failed to unmarsal chapi request: %w", err)
}

connectionID, err := a.AcceptOOBInvitation(agentID, request.DIDCommInvitation, issuerID)
connectionID, err := a.AcceptOOBInvitation(agentID, (*outofband.Invitation)(request.DIDCommInvitation.AsV1()), issuerID)
if err != nil {
return fmt.Errorf("failed to accept oob invitation: %w", err)
}
Expand Down
Loading

0 comments on commit 87a1ad0

Please sign in to comment.