From 5ffe3445fd9027f09067f1d4ef41b56defb919c0 Mon Sep 17 00:00:00 2001 From: Filip Burlacu Date: Tue, 23 Mar 2021 04:18:07 -0400 Subject: [PATCH] fix: fixes for didexchange interop with aca-py 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-request 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 --- cmd/aries-agent-rest/go.mod | 4 +- cmd/aries-agent-rest/go.sum | 6 + cmd/aries-js-worker/go.mod | 2 +- cmd/aries-js-worker/go.sum | 2 + pkg/didcomm/common/service/destination.go | 66 ++++++++++- .../common/service/destination_test.go | 86 +++++++++++++++ pkg/didcomm/common/service/did_comm_msg.go | 8 ++ pkg/didcomm/protocol/didexchange/models.go | 5 + pkg/didcomm/protocol/didexchange/service.go | 47 +++++++- .../protocol/didexchange/service_test.go | 103 +++++++++++++++++- pkg/didcomm/protocol/didexchange/states.go | 9 +- .../protocol/didexchange/states_test.go | 16 +++ pkg/didcomm/transport/http/inbound.go | 9 +- pkg/doc/did/doc.go | 24 ++++ pkg/doc/did/doc_test.go | 65 ++++++++++- pkg/mock/diddoc/mock_diddoc.go | 39 +++++++ 16 files changed, 478 insertions(+), 13 deletions(-) diff --git a/cmd/aries-agent-rest/go.mod b/cmd/aries-agent-rest/go.mod index 6f470e78df..fe71aafbb3 100644 --- a/cmd/aries-agent-rest/go.mod +++ b/cmd/aries-agent-rest/go.mod @@ -12,8 +12,8 @@ require ( github.com/gorilla/mux v1.7.3 github.com/hyperledger/aries-framework-go v0.1.6-0.20210304193329-f56b2cebc386 github.com/hyperledger/aries-framework-go/component/storage/leveldb v0.0.0-20210305152013-b276ca413681 - github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210310001230-bc1bd8ea889c - github.com/hyperledger/aries-framework-go/spi v0.0.0-20210310001230-bc1bd8ea889c + github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210320144851-40976de98ccf + github.com/hyperledger/aries-framework-go/spi v0.0.0-20210320144851-40976de98ccf github.com/rs/cors v1.7.0 github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 // indirect diff --git a/cmd/aries-agent-rest/go.sum b/cmd/aries-agent-rest/go.sum index d514d60a8c..e5001fb49c 100644 --- a/cmd/aries-agent-rest/go.sum +++ b/cmd/aries-agent-rest/go.sum @@ -213,6 +213,8 @@ github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-202102242 github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210224230531-58e1368e5661/go.mod h1:XaPVDJcbQT8BKmThfQdWPc+hgicHFAQzSOavHw2gn/4= github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210310001230-bc1bd8ea889c h1:ELa1dI2zdRegFsaPygDgho+1DKIVUAs0KGnkw7ab304= github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210310001230-bc1bd8ea889c/go.mod h1:zOolL2VqWj6+SPe13nrK0oo5QUOfQZQKB0j1iQsje0k= +github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210320144851-40976de98ccf h1:rh+NCbTrYfJ9ho4NRVdgLuIVF05vuklmnF2JWP1+aFo= +github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210320144851-40976de98ccf/go.mod h1:HVV8sifdHIyLkrlgmK/6+3YWKnOJPUfoNU+4SwQqMSs= github.com/hyperledger/aries-framework-go/spi v0.0.0-20210205153949-f852f978a0d6/go.mod h1:fDr9wW00GJJl1lR1SFHmJW8utIocdvjO5RNhAYS05EY= github.com/hyperledger/aries-framework-go/spi v0.0.0-20210210184327-0b9d0fd4c34e/go.mod h1:fDr9wW00GJJl1lR1SFHmJW8utIocdvjO5RNhAYS05EY= github.com/hyperledger/aries-framework-go/spi v0.0.0-20210224230531-58e1368e5661/go.mod h1:fDr9wW00GJJl1lR1SFHmJW8utIocdvjO5RNhAYS05EY= @@ -221,10 +223,14 @@ github.com/hyperledger/aries-framework-go/spi v0.0.0-20210305152013-b276ca413681 github.com/hyperledger/aries-framework-go/spi v0.0.0-20210305152013-b276ca413681/go.mod h1:fDr9wW00GJJl1lR1SFHmJW8utIocdvjO5RNhAYS05EY= github.com/hyperledger/aries-framework-go/spi v0.0.0-20210310001230-bc1bd8ea889c h1:PzNaY9LS6oP1edqx8CWUf8FifpWb56PGedqf/tAwR3Q= github.com/hyperledger/aries-framework-go/spi v0.0.0-20210310001230-bc1bd8ea889c/go.mod h1:fDr9wW00GJJl1lR1SFHmJW8utIocdvjO5RNhAYS05EY= +github.com/hyperledger/aries-framework-go/spi v0.0.0-20210310160016-d5eea2ecdd50/go.mod h1:fDr9wW00GJJl1lR1SFHmJW8utIocdvjO5RNhAYS05EY= +github.com/hyperledger/aries-framework-go/spi v0.0.0-20210320144851-40976de98ccf h1:5xKqwVy1gEBENyU7a+KCeDOOtF21wrYtHsEicuXWE/c= +github.com/hyperledger/aries-framework-go/spi v0.0.0-20210320144851-40976de98ccf/go.mod h1:fDr9wW00GJJl1lR1SFHmJW8utIocdvjO5RNhAYS05EY= github.com/hyperledger/aries-framework-go/test/component v0.0.0-20210219073333-c46e84ce678f/go.mod h1:/ljIFCu5iDIziwuvObF0vEc3fJ5dgDpT8RYAhQdNeHI= github.com/hyperledger/aries-framework-go/test/component v0.0.0-20210226235232-298aa129d822/go.mod h1:6Za6hvu+eZDPerePXIlMuBWbQZDKqTgOrKV56WZMtcI= github.com/hyperledger/aries-framework-go/test/component v0.0.0-20210304193329-f56b2cebc386 h1:LLJg+gSy+yPGtdYQopGMI4/C4LA9ZTFkomA4c83q0Ow= github.com/hyperledger/aries-framework-go/test/component v0.0.0-20210304193329-f56b2cebc386/go.mod h1:6Za6hvu+eZDPerePXIlMuBWbQZDKqTgOrKV56WZMtcI= +github.com/hyperledger/aries-framework-go/test/component v0.0.0-20210310160016-d5eea2ecdd50/go.mod h1:AybsT4/saiuxdVhK5CgOLIkcNMPZtX3GAUMOjHcLLjk= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= diff --git a/cmd/aries-js-worker/go.mod b/cmd/aries-js-worker/go.mod index ed889f5886..2ee5b8074a 100644 --- a/cmd/aries-js-worker/go.mod +++ b/cmd/aries-js-worker/go.mod @@ -8,7 +8,7 @@ go 1.15 require ( github.com/google/uuid v1.1.2 - github.com/hyperledger/aries-framework-go v0.1.7-0.20210310160016-d5eea2ecdd50 + github.com/hyperledger/aries-framework-go v0.1.7-0.20210320144851-40976de98ccf github.com/hyperledger/aries-framework-go/component/storage/indexeddb v0.0.0-00010101000000-000000000000 github.com/mitchellh/mapstructure v1.3.0 github.com/stretchr/testify v1.7.0 diff --git a/cmd/aries-js-worker/go.sum b/cmd/aries-js-worker/go.sum index 72ce569d9d..cee30c0b48 100644 --- a/cmd/aries-js-worker/go.sum +++ b/cmd/aries-js-worker/go.sum @@ -176,6 +176,8 @@ github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKe github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210310001230-bc1bd8ea889c h1:ELa1dI2zdRegFsaPygDgho+1DKIVUAs0KGnkw7ab304= github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210310001230-bc1bd8ea889c/go.mod h1:zOolL2VqWj6+SPe13nrK0oo5QUOfQZQKB0j1iQsje0k= +github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210320144851-40976de98ccf h1:rh+NCbTrYfJ9ho4NRVdgLuIVF05vuklmnF2JWP1+aFo= +github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20210320144851-40976de98ccf/go.mod h1:HVV8sifdHIyLkrlgmK/6+3YWKnOJPUfoNU+4SwQqMSs= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o= diff --git a/pkg/didcomm/common/service/destination.go b/pkg/didcomm/common/service/destination.go index bd07a5521f..78643798ec 100644 --- a/pkg/didcomm/common/service/destination.go +++ b/pkg/didcomm/common/service/destination.go @@ -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. @@ -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 @@ -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 == "" { @@ -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 +} diff --git a/pkg/didcomm/common/service/destination_test.go b/pkg/didcomm/common/service/destination_test.go index 10fbc5113b..eed7f704dd 100644 --- a/pkg/didcomm/common/service/destination_test.go +++ b/pkg/didcomm/common/service/destination_test.go @@ -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) { @@ -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) diff --git a/pkg/didcomm/common/service/did_comm_msg.go b/pkg/didcomm/common/service/did_comm_msg.go index 4fa5ed8124..09bbe5f284 100644 --- a/pkg/didcomm/common/service/did_comm_msg.go +++ b/pkg/didcomm/common/service/did_comm_msg.go @@ -25,6 +25,9 @@ const ( jsonThreadID = "thid" jsonParentThreadID = "pthid" jsonMetadata = "_internal_metadata" + + oldPIURI = "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/" + newPIURI = "https://didcomm.org/" ) // Metadata may contain additional payload for the protocol. It might be populated by the client/protocol @@ -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, newPIURI, 1) + } + return msg, nil } diff --git a/pkg/didcomm/protocol/didexchange/models.go b/pkg/didcomm/protocol/didexchange/models.go index e4978a7b73..16268fda4b 100644 --- a/pkg/didcomm/protocol/didexchange/models.go +++ b/pkg/didcomm/protocol/didexchange/models.go @@ -74,6 +74,11 @@ type Request struct { Label string `json:"label,omitempty"` Connection *Connection `json:"connection,omitempty"` Thread *decorator.Thread `json:"~thread,omitempty"` + // DID the did of the requester. Optional, may be present within `connection` instead. + DID string `json:"did,omitempty"` + // DocAttach an attachment containing the did doc of the requester. + // Optional, may be present within `connection` instead. + DocAttach *decorator.Attachment `json:"did_doc~attach,omitempty"` } // Response defines a2a DID exchange response diff --git a/pkg/didcomm/protocol/didexchange/service.go b/pkg/didcomm/protocol/didexchange/service.go index 9ecf973d8e..c61191b255 100644 --- a/pkg/didcomm/protocol/didexchange/service.go +++ b/pkg/didcomm/protocol/didexchange/service.go @@ -7,9 +7,11 @@ SPDX-License-Identifier: Apache-2.0 package didexchange import ( + "encoding/base64" "encoding/json" "errors" "fmt" + "strings" "github.com/google/uuid" @@ -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{} @@ -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 } diff --git a/pkg/didcomm/protocol/didexchange/service_test.go b/pkg/didcomm/protocol/didexchange/service_test.go index d3b43e9b59..906e444add 100644 --- a/pkg/didcomm/protocol/didexchange/service_test.go +++ b/pkg/didcomm/protocol/didexchange/service_test.go @@ -9,6 +9,7 @@ package didexchange import ( "crypto/ed25519" "crypto/rand" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -1177,8 +1178,102 @@ func TestInvitationRecord(t *testing.T) { require.Contains(t, err.Error(), "save connection record") } +func Test_getRequestConnection(t *testing.T) { + t.Run("success - connection member exists", func(t *testing.T) { + r := Request{Connection: &Connection{DID: "test", DIDDoc: newPeerDID(t)}} + + conn, err := getRequestConnection(&r) + require.NoError(t, err) + require.Equal(t, "test", conn.DID) + }) + + t.Run("success - did_doc~attach present", func(t *testing.T) { + testDoc := newPeerDID(t) + docBytes, err := testDoc.MarshalJSON() + require.NoError(t, err) + + attachment := decorator.Attachment{ + Data: decorator.AttachmentData{ + Base64: base64.StdEncoding.EncodeToString(docBytes), + }, + } + + r := Request{DID: "test", DocAttach: &attachment} + + conn, err := getRequestConnection(&r) + require.NoError(t, err) + require.Equal(t, "test", conn.DID) + }) + + t.Run("failure - no connection or did_doc~attach", func(t *testing.T) { + _, err := getRequestConnection(&Request{DID: "test"}) + require.Error(t, err) + require.Contains(t, err.Error(), "missing did_doc~attach") + }) + + t.Run("failure - did_doc~attach not valid base64", func(t *testing.T) { + _, err := getRequestConnection(&Request{DID: "test", DocAttach: &decorator.Attachment{ + Data: decorator.AttachmentData{ + Base64: "abc %$#@abc test test bad data ^", + }, + }}) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to parse base64 attachment data") + }) + + t.Run("failure - did_doc~attach not valid did doc", func(t *testing.T) { + _, err := getRequestConnection(&Request{DID: "test", DocAttach: &decorator.Attachment{ + Data: decorator.AttachmentData{ + Base64: base64.StdEncoding.EncodeToString([]byte("this is not a valid did doc")), + }, + }}) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to parse did document") + }) + + t.Run("test pad", func(t *testing.T) { + in := []string{ + "abcdabcdabcdabcda", + "abcdabcdabcdabcdab", + "abcdabcdabcdabcdabc", + "abcdabcdabcdabcdabcd", + "abcdabcdabcdabcdabcda", + "abcdabcdabcdabcdabcdab", + } + + out := []string{ + "abcdabcdabcdabcda", + "abcdabcdabcdabcdab==", + "abcdabcdabcdabcdabc=", + "abcdabcdabcdabcdabcd", + "abcdabcdabcdabcdabcda", + "abcdabcdabcdabcdabcdab==", + } + + for i, input := range in { + require.Equal(t, out[i], pad(input)) + } + }) +} + func TestRequestRecord(t *testing.T) { - t.Run("returns connection reecord", func(t *testing.T) { + t.Run("returns connection record", func(t *testing.T) { + svc, err := New(&protocol.MockProvider{ + ServiceMap: map[string]interface{}{ + mediator.Coordination: &mockroute.MockMediatorSvc{}, + }, + }) + require.NoError(t, err) + + didcommMsg := generateRequestMsgPayload(t, &protocol.MockProvider{}, randomString(), uuid.New().String()) + require.NotEmpty(t, didcommMsg.ParentThreadID()) + conn, err := svc.requestMsgRecord(didcommMsg) + require.NoError(t, err) + require.NotNil(t, conn) + require.Equal(t, didcommMsg.ParentThreadID(), conn.InvitationID) + }) + + t.Run("returns connection record from request without connection", func(t *testing.T) { svc, err := New(&protocol.MockProvider{ ServiceMap: map[string]interface{}{ mediator.Coordination: &mockroute.MockMediatorSvc{}, @@ -1188,10 +1283,14 @@ func TestRequestRecord(t *testing.T) { didcommMsg := generateRequestMsgPayload(t, &protocol.MockProvider{}, randomString(), uuid.New().String()) require.NotEmpty(t, didcommMsg.ParentThreadID()) + delete(didcommMsg, "connection") + didcommMsg["did"] = "did:test:abc" + conn, err := svc.requestMsgRecord(didcommMsg) require.NoError(t, err) require.NotNil(t, conn) require.Equal(t, didcommMsg.ParentThreadID(), conn.InvitationID) + require.Equal(t, "did:test:abc", conn.TheirDID) }) t.Run("fails on db error", func(t *testing.T) { @@ -1751,7 +1850,7 @@ func TestFetchConnectionRecord(t *testing.T) { }) } -func generateRequestMsgPayload(t *testing.T, prov provider, id, invitationID string) service.DIDCommMsg { +func generateRequestMsgPayload(t *testing.T, prov provider, id, invitationID string) service.DIDCommMsgMap { connRec, err := connection.NewRecorder(prov) require.NoError(t, err) require.NotNil(t, connRec) diff --git a/pkg/didcomm/protocol/didexchange/states.go b/pkg/didcomm/protocol/didexchange/states.go index e2a5f0697d..a4bc7d3567 100644 --- a/pkg/didcomm/protocol/didexchange/states.go +++ b/pkg/didcomm/protocol/didexchange/states.go @@ -389,7 +389,12 @@ func (ctx *context) handleInboundInvitation(invitation *Invitation, thid string, func (ctx *context) handleInboundRequest(request *Request, options *options, connRec *connectionstore.Record) (stateAction, *connectionstore.Record, error) { - requestDidDoc, err := ctx.resolveDidDocFromConnection(request.Connection) + reqConn, err := getRequestConnection(request) + if err != nil { + return nil, nil, fmt.Errorf("extracting connection data from request: %w", err) + } + + requestDidDoc, err := ctx.resolveDidDocFromConnection(reqConn) if err != nil { return nil, nil, fmt.Errorf("resolve did doc from exchange request connection: %w", err) } @@ -417,7 +422,7 @@ func (ctx *context) handleInboundRequest(request *Request, options *options, ConnectionSignature: encodedConnectionSignature, } - connRec.TheirDID = request.Connection.DID + connRec.TheirDID = reqConn.DID connRec.MyDID = connection.DID connRec.TheirLabel = request.Label diff --git a/pkg/didcomm/protocol/didexchange/states_test.go b/pkg/didcomm/protocol/didexchange/states_test.go index e361794886..73d75668f9 100644 --- a/pkg/didcomm/protocol/didexchange/states_test.go +++ b/pkg/didcomm/protocol/didexchange/states_test.go @@ -996,6 +996,20 @@ func TestNewResponseFromRequest(t *testing.T) { require.NotNil(t, connRec.MyDID) require.NotNil(t, connRec.TheirDID) }) + + t.Run("unsuccessful new response from request due to get connection error", func(t *testing.T) { + ctx := getContext(t, &prov) + request, err := createRequest(t, ctx) + require.NoError(t, err) + + request.Connection = nil + + _, connRec, err := ctx.handleInboundRequest(request, &options{}, &connection.Record{}) + require.Error(t, err) + require.Contains(t, err.Error(), "extracting connection data") + require.Nil(t, connRec) + }) + t.Run("unsuccessful new response from request due to create did error", func(t *testing.T) { didDoc := mockdiddoc.GetMockDIDDoc(t) ctx := &context{ @@ -1011,6 +1025,7 @@ func TestNewResponseFromRequest(t *testing.T) { require.Contains(t, err.Error(), "create DID error") require.Nil(t, connRec) }) + t.Run("unsuccessful new response from request due to sign error", func(t *testing.T) { connRec, err := connection.NewRecorder(&prov) require.NoError(t, err) @@ -1037,6 +1052,7 @@ func TestNewResponseFromRequest(t *testing.T) { require.Contains(t, err.Error(), "sign error") require.Nil(t, connRecord) }) + t.Run("unsuccessful new response from request due to resolve public did from request error", func(t *testing.T) { ctx := &context{vdRegistry: &mockvdr.MockVDRegistry{ResolveErr: errors.New("resolver error")}} request := &Request{Connection: &Connection{DID: "did:sidetree:abc"}} diff --git a/pkg/didcomm/transport/http/inbound.go b/pkg/didcomm/transport/http/inbound.go index 7908d5e62f..9770dfe29e 100644 --- a/pkg/didcomm/transport/http/inbound.go +++ b/pkg/didcomm/transport/http/inbound.go @@ -21,6 +21,11 @@ import ( var logger = log.New("aries-framework/http") +const ( + // acceptInboundContentType additional content type to be accepted for inbound messages. + acceptInboundContentType = "application/ssi-agent-wire" +) + // TODO https://github.com/hyperledger/aries-framework-go/issues/891 Support for Transport Return Route (Duplex) // NewInboundHandler will create a new handler to enforce Did-Comm HTTP transport specs @@ -98,7 +103,9 @@ func validateHTTPMethod(w http.ResponseWriter, r *http.Request) bool { } ct := r.Header.Get("Content-type") - if ct != commContentType { + + // Interop: accept application/ssi-agent-wire legacy content type for inbound messages + if ct != commContentType && ct != acceptInboundContentType { http.Error(w, fmt.Sprintf("Unsupported Content-type \"%s\"", ct), http.StatusUnsupportedMediaType) return false } diff --git a/pkg/doc/did/doc.go b/pkg/doc/did/doc.go index 59e8a47fe8..4b4bdc32ba 100644 --- a/pkg/doc/did/doc.go +++ b/pkg/doc/did/doc.go @@ -360,6 +360,14 @@ func ParseDocument(data []byte) (*Doc, error) { } else if raw == nil { return nil, errors.New("document payload is not provided") } + + // Interop: handle legacy did docs that incorrectly indicate they use the new format + if requiresLegacyHandling(raw) { + ctx, _ := json.Marshal(raw.Context) + println("legacy:", string(ctx)) + raw.Context = []string{contextV011} + } + // validate did document err = validate(data, raw.schemaLoader()) if err != nil { @@ -404,6 +412,22 @@ func ParseDocument(data []byte) (*Doc, error) { return doc, nil } +func requiresLegacyHandling(raw *rawDoc) bool { + if len(raw.PublicKey) == 0 { // docs in v1 format don't have a top-level publicKey array + return false + } + + context, _ := parseContext(raw.Context) + + for _, ctx := range context { + if ctx == Context { // docs that state they use v1 format but still have a top-level publicKey array + return true + } + } + + return false // docs in older formats +} + func populateVerificationRelationships(doc *Doc, raw *rawDoc) error { authentications, err := populateVerification(doc, raw.Authentication, Authentication) if err != nil { diff --git a/pkg/doc/did/doc_test.go b/pkg/doc/did/doc_test.go index 7b3e844e2e..8208a78daf 100644 --- a/pkg/doc/did/doc_test.go +++ b/pkg/doc/did/doc_test.go @@ -106,6 +106,58 @@ const validDoc = `{ "created": "2002-10-10T17:00:00Z" }` +//nolint:lll +const invalidDoc = `{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:example:21tDAKCERh95uGgKbJNHYp", + "publicKey": [ + { + "id": "did:example:123456789abcdefghi#keys-1", + "type": "Secp256k1VerificationKey2018", + "owner": "did:example:123456789abcdefghi", + "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" + }, + { + "id": "did:example:123456789abcdefghw#key2", + "type": "RsaVerificationKey2018", + "owner": "did:example:123456789abcdefghw", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAryQICCl6NZ5gDKrnSztO\n3Hy8PEUcuyvg/ikC+VcIo2SFFSf18a3IMYldIugqqqZCs4/4uVW3sbdLs/6PfgdX\n7O9D22ZiFWHPYA2k2N744MNiCD1UE+tJyllUhSblK48bn+v1oZHCM0nYQ2NqUkvS\nj+hwUU3RiWl7x3D2s9wSdNt7XUtW05a/FXehsPSiJfKvHJJnGOX0BgTvkLnkAOTd\nOrUZ/wK69Dzu4IvrN4vs9Nes8vbwPa/ddZEzGR0cQMt0JBkhk9kU/qwqUseP1QRJ\n5I1jR4g8aYPL/ke9K35PxZWuDp3U0UPAZ3PjFAh+5T+fc7gzCs9dPzSHloruU+gl\nFQIDAQAB\n-----END PUBLIC KEY-----" + } + ], + "authentication": [ + { + "type": "Secp256k1VerificationKey2018", + "publicKey": "did:example:123456789abcdefghi#keys-1" + }, + { + "id": "did:example:123456789abcdefghs#key3", + "type": "RsaVerificationKey2018", + "owner": "did:example:123456789abcdefghs", + "publicKeyHex": "02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71" + } + ], + "service": [ + { + "id": "did:example:123456789abcdefghi#inbox", + "type": "SocialWebInboxService", + "serviceEndpoint": "https://social.example.com/83hfh37dj", + "spamCost": { + "amount": "0.50", + "currency": "USD" + } + }, + { + "id": "did:example:123456789abcdefghi#did-communication", + "type": "did-communication", + "serviceEndpoint": "https://agent.example.com/", + "priority" : 0, + "recipientKeys" : ["did:example:123456789abcdefghi#key2"], + "routingKeys" : ["did:example:123456789abcdefghi#key2"] + } + ], + "created": "2002-10-10T17:00:00Z" +}` + //nolint:lll const validDocV011 = `{ "@context": ["https://w3id.org/did/v0.11"], @@ -163,7 +215,7 @@ const validDocWithBase = `{ "@context": ["https://w3id.org/did/v1", { "@base": "did:example:123456789abcdefghi"}], "id": "did:example:123456789abcdefghi", - "publicKey": [ + "verificationMethod": [ { "id": "#keys-1", "type": "Secp256k1VerificationKey2018", @@ -424,6 +476,13 @@ func TestValid(t *testing.T) { } } +func TestInvalid(t *testing.T) { + doc, err := ParseDocument([]byte(invalidDoc)) + require.Error(t, err) + require.Nil(t, doc) + require.Contains(t, err.Error(), "did document not valid") +} + func TestValidWithProof(t *testing.T) { docs := []string{validDocWithProof, validDocV011WithProof} for _, d := range docs { @@ -1775,7 +1834,7 @@ const validDocWithProof = `{ "proofValue": "6mdES87erjP5r1qCSRW__otj-A_Rj0YgRO7XU_0Amhwdfa7AAmtGUSFGflR_fZqPYrY9ceLRVQCJ49s0q7-LBA", "type": "Ed25519Signature2018" }], - "publicKey": [{ + "verificationMethod": [{ "controller": "did:method:abc", "id": "did:method:abc#key-1", "publicKeyBase58": "GY4GunSXBPBfhLCzDL7iGmP5dR3sBDCJZkkaGK8VgYQf", @@ -1801,7 +1860,7 @@ const validDocWithProofAndJWK = ` "type": "Ed25519Signature2018" } ], - "publicKey": [ + "verificationMethod": [ { "controller": "did:method:abc", "id": "did:method:abc#key-1", diff --git a/pkg/mock/diddoc/mock_diddoc.go b/pkg/mock/diddoc/mock_diddoc.go index c4d4b73656..8acb8c7ed0 100644 --- a/pkg/mock/diddoc/mock_diddoc.go +++ b/pkg/mock/diddoc/mock_diddoc.go @@ -56,6 +56,45 @@ func GetMockDIDDoc(t *testing.T) *did.Doc { } } +// GetMockIndyDoc creates a mock DID Doc for testing. +func GetMockIndyDoc(t *testing.T) *did.Doc { + t.Helper() + + return &did.Doc{ + Context: []string{"https://w3id.org/did/v1"}, + ID: "did:sov:AyRHrP7u6rF1dKViGf5shA", + VerificationMethod: []did.VerificationMethod{ + { + ID: "did:sov:AyRHrP7u6rF1dKViGf5shA#1", + Type: "Ed25519VerificationKey2018", + Controller: "did:sov:AyRHrP7u6rF1dKViGf5shA", + Value: base58.Decode("6SFxbqdqGKtVVmLvXDnq9JP4ziZCG2fJzETpMYHt1VNx"), + }, + }, + Service: []did.Service{ + { + ID: "did:sov:AyRHrP7u6rF1dKViGf5shA;indy", + Type: "IndyAgent", + Priority: 0, + RecipientKeys: []string{"6SFxbqdqGKtVVmLvXDnq9JP4ziZCG2fJzETpMYHt1VNx"}, + ServiceEndpoint: "https://localhost:8090", + }, + }, + Authentication: []did.Verification{ + { + VerificationMethod: did.VerificationMethod{ + ID: "did:sov:AyRHrP7u6rF1dKViGf5shA#1", + Type: "Ed25519VerificationKey2018", + Controller: "did:sov:AyRHrP7u6rF1dKViGf5shA", + Value: base58.Decode("6SFxbqdqGKtVVmLvXDnq9JP4ziZCG2fJzETpMYHt1VNx"), + }, + Relationship: 1, + Embedded: false, + }, + }, + } +} + // MockDIDKey returns a new did:key DID for testing purposes. func MockDIDKey(t *testing.T) string { t.Helper()