Skip to content

Commit

Permalink
fix: fixes for didexchange interop with aca-py (hyperledger-archives#…
Browse files Browse the repository at this point in the history
…2655)

accept old and new didex. message formats and mime types:
- application/ssi-agent-wire is accepted as an inbound mime type
- did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0 is accepted as
  an inbound PIURI for didexchange messages
- did doc can be parsed from "did_doc~attach" attachment in request
  messages, as per didexchange spec, in addition to being accepted
  inside "connection" field, as per connection-protocol spec

did doc format:
- Parse legacy peer did docs with publicKey members even if their
  context states they are using a newer format.
- Translate raw ed25519 public key values to did:key when reading
  recipient keys or routing keys

Signed-off-by: Filip Burlacu <[email protected]>
  • Loading branch information
Moopli authored and sudeshrshetty committed Oct 14, 2021
1 parent 7fd8418 commit fe56935
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 15 deletions.
66 changes: 65 additions & 1 deletion pkg/didcomm/common/service/destination.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ package service
import (
"fmt"

"github.com/btcsuite/btcutil/base58"

diddoc "github.com/hyperledger/aries-framework-go/pkg/doc/did"
vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr"
"github.com/hyperledger/aries-framework-go/pkg/vdr/fingerprint"
)

// Destination provides the recipientKeys, routingKeys, and serviceEndpoint for an outbound message.
Expand All @@ -23,6 +26,8 @@ type Destination struct {

const (
didCommServiceType = "did-communication"
// legacyDIDCommServiceType is the non-spec service type used by legacy didcomm agent systems.
legacyDIDCommServiceType = "IndyAgent"
)

// GetDestination constructs a Destination struct based on the given DID and parameters
Expand All @@ -41,7 +46,8 @@ func GetDestination(did string, vdr vdrapi.Registry) (*Destination, error) {
func CreateDestination(didDoc *diddoc.Doc) (*Destination, error) {
didCommService, ok := diddoc.LookupService(didDoc, didCommServiceType)
if !ok {
return nil, fmt.Errorf("create destination: missing DID doc service")
// Interop: legacy docs may use the IndyAgent service type for didcomm, with slightly different content format.
return createDestinationFromIndy(didDoc)
}

if didCommService.ServiceEndpoint == "" {
Expand All @@ -61,3 +67,61 @@ func CreateDestination(didDoc *diddoc.Doc) (*Destination, error) {
RoutingKeys: didCommService.RoutingKeys,
}, nil
}

func createDestinationFromIndy(didDoc *diddoc.Doc) (*Destination, error) {
didCommService, ok := diddoc.LookupService(didDoc, legacyDIDCommServiceType)
if !ok {
return nil, fmt.Errorf("create destination: missing DID doc service")
}

if didCommService.ServiceEndpoint == "" {
return nil, fmt.Errorf("create destination: no service endpoint on didcomm service block in diddoc: %+v", didDoc)
}

if len(didCommService.RecipientKeys) == 0 {
return nil, fmt.Errorf("create destination: no recipient keys on didcomm service block in diddoc: %+v", didDoc)
}

// TODO ensure recipient keys are did:key's
// https://github.com/hyperledger/aries-framework-go/issues/1604

// convert plain base58 keys to did:key
recKeys := lookupIndyRecipientKeys(didDoc, didCommService.RecipientKeys)
routeKeys := lookupIndyRecipientKeys(didDoc, didCommService.RoutingKeys)

return &Destination{
RecipientKeys: recKeys,
ServiceEndpoint: didCommService.ServiceEndpoint,
RoutingKeys: routeKeys,
}, nil
}

func lookupIndyRecipientKeys(didDoc *diddoc.Doc, recipientKeys []string) []string {
b58VMkeys := map[string]int{}

for i, vm := range didDoc.VerificationMethod {
b58Key := base58.Encode(vm.Value)
b58VMkeys[b58Key] = i
}

var didKeys []string

for _, key := range recipientKeys {
vmIdx, ok := b58VMkeys[key]
if !ok {
continue
}

vm := didDoc.VerificationMethod[vmIdx]
if vm.Type != "Ed25519VerificationKey2018" {
// TODO: handle further key types
continue
}

didKey, _ := fingerprint.CreateDIDKey(vm.Value)

didKeys = append(didKeys, didKey)
}

return didKeys
}
86 changes: 86 additions & 0 deletions pkg/didcomm/common/service/destination_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/hyperledger/aries-framework-go/pkg/doc/did"
mockdiddoc "github.com/hyperledger/aries-framework-go/pkg/mock/diddoc"
mockvdr "github.com/hyperledger/aries-framework-go/pkg/mock/vdr"
"github.com/hyperledger/aries-framework-go/pkg/vdr/fingerprint"
)

func TestGetDestinationFromDID(t *testing.T) {
Expand Down Expand Up @@ -110,6 +111,91 @@ func TestPrepareDestination(t *testing.T) {
})
}

func TestCreateDestinationFromLegacyDoc(t *testing.T) {
t.Run("successfully prepared destination", func(t *testing.T) {
doc := mockdiddoc.GetMockIndyDoc(t)
dest, err := CreateDestination(doc)
require.NoError(t, err)
require.NotNil(t, dest)
require.Equal(t, dest.ServiceEndpoint, "https://localhost:8090")
require.Equal(t, doc.Service[0].RoutingKeys, dest.RoutingKeys)
})

t.Run("error while getting service", func(t *testing.T) {
didDoc := mockdiddoc.GetMockIndyDoc(t)
didDoc.Service = nil

dest, err := createDestinationFromIndy(didDoc)
require.Error(t, err)
require.Contains(t, err.Error(), "missing DID doc service")
require.Nil(t, dest)
})

t.Run("missing service endpoint", func(t *testing.T) {
didDoc := mockdiddoc.GetMockIndyDoc(t)
didDoc.Service = []did.Service{{
Type: legacyDIDCommServiceType,
}}

dest, err := createDestinationFromIndy(didDoc)
require.Error(t, err)
require.Contains(t, err.Error(), "no service endpoint")
require.Nil(t, dest)
})

t.Run("missing recipient keys", func(t *testing.T) {
didDoc := mockdiddoc.GetMockIndyDoc(t)
didDoc.Service = []did.Service{{
Type: legacyDIDCommServiceType,
ServiceEndpoint: "localhost:8080",
}}

dest, err := createDestinationFromIndy(didDoc)
require.Error(t, err)
require.Contains(t, err.Error(), "no recipient keys")
require.Nil(t, dest)
})
}

func TestLookupIndyKeys(t *testing.T) {
t.Run("lookup recipient keys", func(t *testing.T) {
didDoc := mockdiddoc.GetMockIndyDoc(t)

recipientKeys := lookupIndyRecipientKeys(didDoc, didDoc.Service[0].RecipientKeys)
require.NotNil(t, recipientKeys)
require.Len(t, recipientKeys, 1)

pk, err := fingerprint.PubKeyFromDIDKey(recipientKeys[0])
require.NoError(t, err)
require.ElementsMatch(t, didDoc.VerificationMethod[0].Value, pk)
})

t.Run("no keys to lookup", func(t *testing.T) {
didDoc := mockdiddoc.GetMockIndyDoc(t)

recipientKeys := lookupIndyRecipientKeys(didDoc, nil)
require.Nil(t, recipientKeys)
})

t.Run("skip key that isn't found in verificationmethod list", func(t *testing.T) {
didDoc := mockdiddoc.GetMockIndyDoc(t)

didDoc.Service[0].RecipientKeys = []string{"bad key"}

recipientKeys := lookupIndyRecipientKeys(didDoc, didDoc.Service[0].RecipientKeys)
require.Len(t, recipientKeys, 0)
})

t.Run("skip keys that aren't of handled type", func(t *testing.T) {
didDoc := mockdiddoc.GetMockIndyDoc(t)

didDoc.VerificationMethod[0].Type = "bad type"

recipientKeys := lookupIndyRecipientKeys(didDoc, didDoc.Service[0].RecipientKeys)
require.Len(t, recipientKeys, 0)
})
}

func createDIDDoc() *did.Doc {
pubKey, _ := generateKeyPair()
return createDIDDocWithKey(pubKey)
Expand Down
8 changes: 8 additions & 0 deletions pkg/didcomm/common/service/did_comm_msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ const (
jsonThreadID = "thid"
jsonParentThreadID = "pthid"
jsonMetadata = "_internal_metadata"

basePIURI = "https://didcomm.org/"
oldPIURI = "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/"
)

// Metadata may contain additional payload for the protocol. It might be populated by the client/protocol
Expand Down Expand Up @@ -76,6 +79,11 @@ func ParseDIDCommMsgMap(payload []byte) (DIDCommMsgMap, error) {
return nil, fmt.Errorf("invalid payload data format: %w", err)
}

// Interop: accept old PIURI when it's used, as we handle backwards-compatibility at a more fine-grained level.
if typ := msg.Type(); typ != "" {
msg[jsonType] = strings.Replace(typ, oldPIURI, basePIURI, 1)
}

return msg, nil
}

Expand Down
17 changes: 12 additions & 5 deletions pkg/didcomm/protocol/didexchange/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,18 @@ type Invitation struct {
// Request defines a2a DID exchange request
// https://github.com/hyperledger/aries-rfcs/tree/master/features/0023-did-exchange#1-exchange-request
type Request struct {
Type string `json:"@type,omitempty"`
ID string `json:"@id,omitempty"`
Label string `json:"label,omitempty"`
Connection *Connection `json:"connection,omitempty"`
Thread *decorator.Thread `json:"~thread,omitempty"`
Type string `json:"@type,omitempty"`
ID string `json:"@id,omitempty"`
Label string `json:"label,omitempty"`
Thread *decorator.Thread `json:"~thread,omitempty"`
// DID the did of the requester.
// Mandatory in did-exchange, but optional for backwards-compatibility with rfc 0160 connection protocol.
DID string `json:"did,omitempty"`
// DocAttach an attachment containing the did doc of the requester.
// Optional, a requester may provide a publicly-resolvable DID, rather than including an attached did doc.
DocAttach *decorator.Attachment `json:"did_doc~attach,omitempty"`
// Connection is used for backwards-compatibility with rfc 0160 connection protocol.
Connection *Connection `json:"connection,omitempty"`
}

// Response defines a2a DID exchange response
Expand Down
47 changes: 46 additions & 1 deletion pkg/didcomm/protocol/didexchange/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ SPDX-License-Identifier: Apache-2.0
package didexchange

import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/google/uuid"

Expand Down Expand Up @@ -722,6 +724,43 @@ func (s *Service) invitationMsgRecord(msg service.DIDCommMsg) (*connection.Recor
return connRecord, nil
}

// nolint:gomnd
func pad(b64 string) string {
mod := len(b64) % 4
if mod <= 1 {
return b64
}

return b64 + strings.Repeat("=", 4-mod)
}

func getRequestConnection(r *Request) (*Connection, error) {
// Interop: accept the 'connection' attribute of rfc 0160 (connection protocol) if present
if r.Connection != nil {
return r.Connection, nil
}

if r.DocAttach == nil {
return nil, fmt.Errorf("missing did_doc~attach from request")
}

docData, err := base64.StdEncoding.DecodeString(pad(r.DocAttach.Data.Base64))
if err != nil {
return nil, fmt.Errorf("failed to parse base64 attachment data: %w", err)
}

doc, err := did.ParseDocument(docData)
if err != nil {
logger.Errorf("doc bytes: '%s'", string(docData))
return nil, fmt.Errorf("failed to parse did document: %w", err)
}

return &Connection{
DID: r.DID,
DIDDoc: doc,
}, nil
}

func (s *Service) requestMsgRecord(msg service.DIDCommMsg) (*connection.Record, error) {
request := Request{}

Expand All @@ -740,11 +779,17 @@ func (s *Service) requestMsgRecord(msg service.DIDCommMsg) (*connection.Record,
ConnectionID: generateRandomID(),
ThreadID: request.ID,
State: stateNameNull,
TheirDID: request.Connection.DID,
InvitationID: invitationID,
Namespace: theirNSPrefix,
}

// Interop: read their DID from the connection attribute if present
if request.Connection != nil {
connRecord.TheirDID = request.Connection.DID
} else {
connRecord.TheirDID = request.DID
}

if err := s.connectionRecorder.SaveConnectionRecord(connRecord); err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit fe56935

Please sign in to comment.