diff --git a/pkg/didcomm/dispatcher/inbound/inbound_message_handler.go b/pkg/didcomm/dispatcher/inbound/inbound_message_handler.go index bf39bdb83a..becf34861a 100644 --- a/pkg/didcomm/dispatcher/inbound/inbound_message_handler.go +++ b/pkg/didcomm/dispatcher/inbound/inbound_message_handler.go @@ -155,9 +155,20 @@ func (handler *MessageHandler) HandleInboundEnvelope(envelope *transport.Envelop } if foundService != nil { + props := make(map[string]interface{}) + switch foundService.Name() { - // perf: DID exchange and legacy-connection doesn't require myDID and theirDID - case didexchange.DIDExchange, legacyconnection.LegacyConnection: + // perf: DID exchange doesn't require myDID and theirDID + case didexchange.DIDExchange: + // perf: legacy-connection requires envelope.ToKey when sending Connection Response (it will sign connection + // data with this key) + case legacyconnection.LegacyConnection: + // When type of envelope.Message is connections/request, the key which was used to decrypt message is the + // same key which was sent during invitation. If ParentThreadID is missed (Interop issues), that key will be + // used to sign connection-data while sending connection response + if msg.Type() == legacyconnection.RequestMsgType && msg.ParentThreadID() == "" { + props[legacyconnection.InvitationRecipientKey] = base58.Encode(envelope.ToKey) + } default: if !gotDIDs { myDID, theirDID, err = handler.getDIDs(envelope, msg) @@ -167,7 +178,7 @@ func (handler *MessageHandler) HandleInboundEnvelope(envelope *transport.Envelop } } - _, err = foundService.HandleInbound(msg, service.NewDIDCommContext(myDID, theirDID, nil)) + _, err = foundService.HandleInbound(msg, service.NewDIDCommContext(myDID, theirDID, props)) return err } diff --git a/pkg/didcomm/protocol/legacyconnection/keys.go b/pkg/didcomm/protocol/legacyconnection/keys.go index 0f29765146..4f797a28c1 100644 --- a/pkg/didcomm/protocol/legacyconnection/keys.go +++ b/pkg/didcomm/protocol/legacyconnection/keys.go @@ -27,9 +27,8 @@ func (ctx *context) createNewKeyAndVM(didDoc *did.Doc) error { } didDoc.VerificationMethod = append(didDoc.VerificationMethod, *vm) - didDoc.Authentication = append(didDoc.Authentication, *did.NewEmbeddedVerification(vm, did.Authentication)) - // FIXME is KeyAgreement needed? - didDoc.KeyAgreement = append(didDoc.KeyAgreement, *did.NewEmbeddedVerification(kaVM, did.KeyAgreement)) + didDoc.Authentication = append(didDoc.Authentication, *did.NewReferencedVerification(vm, did.Authentication)) + didDoc.KeyAgreement = append(didDoc.KeyAgreement, *did.NewReferencedVerification(kaVM, did.KeyAgreement)) return nil } diff --git a/pkg/didcomm/protocol/legacyconnection/models.go b/pkg/didcomm/protocol/legacyconnection/models.go index b196739676..517b595ab6 100644 --- a/pkg/didcomm/protocol/legacyconnection/models.go +++ b/pkg/didcomm/protocol/legacyconnection/models.go @@ -7,6 +7,13 @@ SPDX-License-Identifier: Apache-2.0 package legacyconnection import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator" "github.com/hyperledger/aries-framework-go/pkg/doc/did" ) @@ -76,3 +83,76 @@ type Connection struct { DID string `json:"DID,omitempty"` DIDDoc *did.Doc `json:"DIDDoc,omitempty"` } + +type connectionRaw struct { + DID string `json:"DID,omitempty"` + DIDDoc *rawDoc `json:"DIDDoc,omitempty"` +} + +type rawDoc struct { + Context interface{} `json:"@context,omitempty"` + ID string `json:"id,omitempty"` + AlsoKnownAs []interface{} `json:"alsoKnownAs,omitempty"` + VerificationMethod []map[string]interface{} `json:"verificationMethod,omitempty"` + PublicKey []map[string]interface{} `json:"publicKey,omitempty"` + Service []map[string]interface{} `json:"service,omitempty"` + Authentication []interface{} `json:"authentication,omitempty"` + AssertionMethod []interface{} `json:"assertionMethod,omitempty"` + CapabilityDelegation []interface{} `json:"capabilityDelegation,omitempty"` + CapabilityInvocation []interface{} `json:"capabilityInvocation,omitempty"` + KeyAgreement []interface{} `json:"keyAgreement,omitempty"` + Created *time.Time `json:"created,omitempty"` + Updated *time.Time `json:"updated,omitempty"` + Proof []interface{} `json:"proof,omitempty"` +} + +// JSONBytes converts Connection to json bytes. +func (con *Connection) JSONBytes() ([]byte, error) { + legacyDoc, err := con.DIDDoc.ToLegacyRawDoc() + if err != nil { + return nil, fmt.Errorf("converting to Legacy Raw Doc failed: %w", err) + } + + connDoc := &rawDoc{} + + _ = mapstructure.Decode(legacyDoc, connDoc) //nolint: errcheck + + conRaw := connectionRaw{ + DID: con.DID, + DIDDoc: connDoc, + } + + byteConn, err := json.Marshal(conRaw) + if err != nil { + return nil, fmt.Errorf("JSON marshalling of connection raw failed: %w", err) + } + + return byteConn, nil +} + +// ParseConnection creates an instance of Connection by reading a JSON connection from bytes. +func ParseConnection(data []byte) (*Connection, error) { + raw := &connectionRaw{} + + err := json.Unmarshal(data, &raw) + if err != nil { + return nil, fmt.Errorf("JSON umarshalling of connection data bytes failed: %w", err) + } else if raw == nil { + return nil, errors.New("document payload is not provided") + } + + docRaw, err := json.Marshal(raw.DIDDoc) + if err != nil { + return nil, fmt.Errorf("JSON marshaling failed: %w", err) + } + + doc, err := did.ParseDocument(docRaw) + if err != nil { + return nil, fmt.Errorf("parcing did document failed: %w", err) + } + + return &Connection{ + DID: raw.DID, + DIDDoc: doc, + }, nil +} diff --git a/pkg/didcomm/protocol/legacyconnection/service.go b/pkg/didcomm/protocol/legacyconnection/service.go index e30296cd2c..97cc9581ec 100644 --- a/pkg/didcomm/protocol/legacyconnection/service.go +++ b/pkg/didcomm/protocol/legacyconnection/service.go @@ -46,7 +46,7 @@ const ( // ResponseMsgType defines the legacy-connection response message type. ResponseMsgType = PIURI + "/response" // AckMsgType defines the legacy-connection ack message type. - AckMsgType = PIURI + "/ack" + AckMsgType = "https://didcomm.org/notification/1.0/ack" routerConnsMetadataKey = "routerConnections" ) @@ -55,6 +55,8 @@ const ( // TODO: https://github.com/hyperledger/aries-framework-go/issues/556 It will not be constant, this namespace // will need to be figured with verification key theirNSPrefix = "their" + // InvitationRecipientKey is map key constant. + InvitationRecipientKey = "invRecipientKey" ) // message type to store data for eventing. This is retrieved during callback. @@ -222,7 +224,7 @@ func retrievingRouterConnections(msg service.DIDCommMsg) []string { } // HandleInbound handles inbound connection messages. -func (s *Service) HandleInbound(msg service.DIDCommMsg, _ service.DIDCommContext) (string, error) { +func (s *Service) HandleInbound(msg service.DIDCommMsg, ctx service.DIDCommContext) (string, error) { logger.Debugf("receive inbound message : %s", msg) // fetch the thread id @@ -238,7 +240,7 @@ func (s *Service) HandleInbound(msg service.DIDCommMsg, _ service.DIDCommContext } // connection record - connRecord, err := s.connectionRecord(msg) + connRecord, err := s.connectionRecord(msg, ctx) if err != nil { return "", fmt.Errorf("failed to fetch connection record : %w", err) } @@ -376,7 +378,7 @@ func (s *Service) handle(msg *message, aEvent chan<- service.DIDCommAction) erro } if connectionRecord.State == StateIDCompleted { - err = s.connectionStore.SaveDIDByResolving(connectionRecord.TheirDID, connectionRecord.RecipientKeys...) + err = s.connectionStore.SaveDID(connectionRecord.TheirDID, connectionRecord.RecipientKeys...) if err != nil { return fmt.Errorf("save theirDID: %w", err) } @@ -627,12 +629,12 @@ func (s *Service) CreateConnection(record *connection.Record, theirDID *did.Doc) return s.connectionRecorder.SaveConnectionRecord(record) } -func (s *Service) connectionRecord(msg service.DIDCommMsg) (*connection.Record, error) { +func (s *Service) connectionRecord(msg service.DIDCommMsg, ctx service.DIDCommContext) (*connection.Record, error) { switch msg.Type() { case InvitationMsgType: return s.invitationMsgRecord(msg) case RequestMsgType: - return s.requestMsgRecord(msg) + return s.requestMsgRecord(msg, ctx) case ResponseMsgType: return s.responseMsgRecord(msg) case AckMsgType: @@ -680,8 +682,11 @@ func (s *Service) invitationMsgRecord(msg service.DIDCommMsg) (*connection.Recor return connRecord, nil } -func (s *Service) requestMsgRecord(msg service.DIDCommMsg) (*connection.Record, error) { - request := Request{} +func (s *Service) requestMsgRecord(msg service.DIDCommMsg, ctx service.DIDCommContext) (*connection.Record, error) { + var ( + request Request + invitationRecKey []string + ) err := msg.Decode(&request) if err != nil { @@ -690,7 +695,12 @@ func (s *Service) requestMsgRecord(msg service.DIDCommMsg) (*connection.Record, invitationID := msg.ParentThreadID() if invitationID == "" { - return nil, fmt.Errorf("missing parent thread ID on connection request with @id=%s", request.ID) + // try to retrieve key which was assigned at HandleInboundEnvelope method of inbound handler + if verKey, ok := ctx.All()[InvitationRecipientKey].(string); ok && verKey != "" { + invitationRecKey = append(invitationRecKey, verKey) + } else { + return nil, fmt.Errorf("missing parent thread ID on connection request with @id=%s", request.ID) + } } if request.Connection == nil { @@ -698,14 +708,15 @@ func (s *Service) requestMsgRecord(msg service.DIDCommMsg) (*connection.Record, } connRecord := &connection.Record{ - TheirLabel: request.Label, - ConnectionID: generateRandomID(), - ThreadID: request.ID, - State: stateNameNull, - TheirDID: request.Connection.DID, - InvitationID: invitationID, - Namespace: theirNSPrefix, - DIDCommVersion: service.V1, + TheirLabel: request.Label, + ConnectionID: generateRandomID(), + ThreadID: request.ID, + State: stateNameNull, + TheirDID: request.Connection.DID, + InvitationID: invitationID, + InvitationRecipientKeys: invitationRecKey, + Namespace: theirNSPrefix, + DIDCommVersion: service.V1, } if !strings.HasPrefix(connRecord.TheirDID, "did") { diff --git a/pkg/didcomm/protocol/legacyconnection/service_test.go b/pkg/didcomm/protocol/legacyconnection/service_test.go index 18e31bff78..80c9fc89b2 100644 --- a/pkg/didcomm/protocol/legacyconnection/service_test.go +++ b/pkg/didcomm/protocol/legacyconnection/service_test.go @@ -409,7 +409,7 @@ func TestService_Handle_Invitee(t *testing.T) { require.NoError(t, err) require.Equal(t, invitation.ServiceEndpoint, uri) - connectionSignature, err := ctx.prepareConnectionSignature(doc.DIDDocument, invitation.ID) + connectionSignature, err := ctx.prepareConnectionSignature(doc.DIDDocument, invitation.RecipientKeys[0]) require.NoError(t, err) // Bob replies with a Response @@ -578,7 +578,7 @@ func TestService_Accept(t *testing.T) { require.Equal(t, true, s.Accept("https://didcomm.org/connections/1.0/invitation")) require.Equal(t, true, s.Accept("https://didcomm.org/connections/1.0/request")) require.Equal(t, true, s.Accept("https://didcomm.org/connections/1.0/response")) - require.Equal(t, true, s.Accept("https://didcomm.org/connections/1.0/ack")) + require.Equal(t, true, s.Accept("https://didcomm.org/notification/1.0/ack")) require.Equal(t, false, s.Accept("unsupported msg type")) } @@ -1215,7 +1215,7 @@ func TestConnectionRecord(t *testing.T) { require.NoError(t, err) conn, err := svc.connectionRecord(generateRequestMsgPayload(t, &protocol.MockProvider{}, - randomString(), randomString())) + randomString(), randomString()), service.EmptyDIDCommContext()) require.NoError(t, err) require.NotNil(t, conn) @@ -1227,7 +1227,7 @@ func TestConnectionRecord(t *testing.T) { msg, err := service.ParseDIDCommMsgMap(requestBytes) require.NoError(t, err) - _, err = svc.connectionRecord(msg) + _, err = svc.connectionRecord(msg, service.EmptyDIDCommContext()) require.Error(t, err) require.Contains(t, err.Error(), "invalid message type") } @@ -1315,7 +1315,7 @@ func TestRequestRecord(t *testing.T) { didcommMsg := generateRequestMsgPayload(t, &protocol.MockProvider{}, randomString(), uuid.New().String()) require.NotEmpty(t, didcommMsg.ParentThreadID()) - conn, err := svc.requestMsgRecord(didcommMsg) + conn, err := svc.requestMsgRecord(didcommMsg, service.EmptyDIDCommContext()) require.NoError(t, err) require.NotNil(t, conn) require.Equal(t, didcommMsg.ParentThreadID(), conn.InvitationID) @@ -1333,7 +1333,7 @@ func TestRequestRecord(t *testing.T) { require.NotEmpty(t, didcommMsg.ParentThreadID()) delete(didcommMsg, "connection") - _, err = svc.requestMsgRecord(didcommMsg) + _, err = svc.requestMsgRecord(didcommMsg, service.EmptyDIDCommContext()) require.Error(t, err) require.Contains(t, err.Error(), "missing connection field") }) @@ -1351,7 +1351,7 @@ func TestRequestRecord(t *testing.T) { require.NoError(t, err) _, err = svc.requestMsgRecord(generateRequestMsgPayload(t, &protocol.MockProvider{}, - randomString(), uuid.New().String())) + randomString(), uuid.New().String()), service.EmptyDIDCommContext()) require.Error(t, err) require.Contains(t, err.Error(), "save connection record") }) @@ -1367,7 +1367,7 @@ func TestRequestRecord(t *testing.T) { parentThreadID := "" didcommMsg := generateRequestMsgPayload(t, &protocol.MockProvider{}, randomString(), parentThreadID) require.Empty(t, didcommMsg.ParentThreadID()) - _, err = svc.requestMsgRecord(didcommMsg) + _, err = svc.requestMsgRecord(didcommMsg, service.EmptyDIDCommContext()) require.Error(t, err) }) } diff --git a/pkg/didcomm/protocol/legacyconnection/states.go b/pkg/didcomm/protocol/legacyconnection/states.go index 782c38e9cb..850944f7cb 100644 --- a/pkg/didcomm/protocol/legacyconnection/states.go +++ b/pkg/didcomm/protocol/legacyconnection/states.go @@ -9,7 +9,6 @@ package legacyconnection import ( "encoding/base64" "encoding/binary" - "encoding/json" "errors" "fmt" "strings" @@ -28,6 +27,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/doc/did" "github.com/hyperledger/aries-framework-go/pkg/doc/util/kmsdidkey" vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" + "github.com/hyperledger/aries-framework-go/pkg/internal/didkeyutil" "github.com/hyperledger/aries-framework-go/pkg/kms" "github.com/hyperledger/aries-framework-go/pkg/kms/localkms" connectionstore "github.com/hyperledger/aries-framework-go/pkg/store/connection" @@ -346,8 +346,18 @@ func (ctx *context) handleInboundRequest(request *Request, options *options, return nil, nil, fmt.Errorf("get response did doc and connection: %w", err) } + var verKey string + if len(connRec.InvitationRecipientKeys) > 0 { + verKey = connRec.InvitationRecipientKeys[0] + } else { + verKey, err = ctx.getVerKey(request.Thread.PID) + if err != nil { + return nil, nil, fmt.Errorf("failed to get verkey : %w", err) + } + } + // prepare connection signature - connectionSignature, err := ctx.prepareConnectionSignature(responseDidDoc, request.Thread.PID) + connectionSignature, err := ctx.prepareConnectionSignature(responseDidDoc, verKey) if err != nil { return nil, nil, err } @@ -364,6 +374,7 @@ func (ctx *context) handleInboundRequest(request *Request, options *options, connRec.MyDID = responseDidDoc.ID connRec.TheirDID = request.Connection.DID connRec.TheirLabel = request.Label + connRec.RecipientKeys = destination.RecipientKeys accept, err := destination.ServiceEndpoint.Accept() if err != nil { @@ -379,16 +390,14 @@ func (ctx *context) handleInboundRequest(request *Request, options *options, }, connRec, nil } -//nolint:funlen -func (ctx *context) prepareConnectionSignature(didDoc *did.Doc, - invitationID string) (*ConnectionSignature, error) { +func (ctx *context) prepareConnectionSignature(didDoc *did.Doc, verKey string) (*ConnectionSignature, error) { connection := &Connection{ DID: didDoc.ID, DIDDoc: didDoc, } - logger.Debugf("connection=%+v invitationID=%s", connection, invitationID) + logger.Debugf("connection=%+v verKey=%s", connection, verKey) - connAttributeBytes, err := json.Marshal(connection) + connAttributeBytes, err := connection.JSONBytes() if err != nil { return nil, fmt.Errorf("failed to marshal connection : %w", err) } @@ -399,11 +408,6 @@ func (ctx *context) prepareConnectionSignature(didDoc *did.Doc, concatenateSignData := append(timestampBuf, connAttributeBytes...) - verKey, err := ctx.getVerKey(invitationID) - if err != nil { - return nil, fmt.Errorf("failed to get verkey : %w", err) - } - var signingKey []byte if strings.HasPrefix(verKey, "did:key:") { @@ -544,6 +548,7 @@ func (ctx *context) getMyDIDDoc(pubDID string, routerConnections []string, servi switch serviceType { case didCommServiceType, legacyDIDCommServiceType: + routingKeys = didkeyutil.ConvertDIDKeysToBase58Keys(routingKeys) svc = did.Service{ Type: serviceType, ServiceEndpoint: model.NewDIDCommV1Endpoint(serviceEndpoint), @@ -621,10 +626,21 @@ func (ctx *context) isPrivateDIDMethod(method string) bool { return true } - return method == "peer" || method == "sov" + return method == "peer" || method == "sov" || method == "key" } func (ctx *context) resolveDidDocFromConnection(con *Connection) (*did.Doc, error) { + if con.DIDDoc == nil { + return nil, fmt.Errorf("missing DIDDoc") + } + // FIXME Interop: aca-py and vcx issue. Should be removed after theirs fix + if !strings.HasPrefix(con.DIDDoc.ID, "did") && len(con.DIDDoc.Service) > 0 { + if len(con.DIDDoc.Service[0].RecipientKeys) > 0 { + con.DIDDoc.ID = didkeyutil.ConvertBase58KeysToDIDKeys(con.DIDDoc.Service[0].RecipientKeys)[0] + con.DID = con.DIDDoc.ID + } + } + parsedDID, err := did.Parse(con.DID) if err != nil { return nil, fmt.Errorf("failed to parse did: %w", err) @@ -638,20 +654,8 @@ func (ctx *context) resolveDidDocFromConnection(con *Connection) (*did.Doc, erro return docResolution.DIDDocument, nil } - - if con.DIDDoc == nil { - return nil, fmt.Errorf("missing DIDDoc") - } - - var method string - - if parsedDID != nil && parsedDID.Method != "sov" { - method = parsedDID.Method - } else { - method = "peer" - } // store provided did document - _, err = ctx.vdRegistry.Create(method, con.DIDDoc, vdrapi.WithOption("store", true)) + _, err = ctx.vdRegistry.Create("peer", con.DIDDoc, vdrapi.WithOption("store", true)) if err != nil { return nil, fmt.Errorf("failed to store provided did document: %w", err) } @@ -659,6 +663,7 @@ func (ctx *context) resolveDidDocFromConnection(con *Connection) (*did.Doc, erro return con.DIDDoc, nil } +// nolint:funlen func (ctx *context) handleInboundResponse(response *Response) (stateAction, *connectionstore.Record, error) { ack := model2.Ack{ Type: AckMsgType, @@ -759,9 +764,8 @@ func (ctx *context) verifySignature(connSignature *ConnectionSignature, recipien } connBytes := sigData[timestampLength:] - conn := &Connection{} - err = json.Unmarshal(connBytes, conn) + conn, err := ParseConnection(connBytes) if err != nil { return nil, fmt.Errorf("JSON unmarshalling of connection: %w", err) } diff --git a/pkg/didcomm/protocol/legacyconnection/states_test.go b/pkg/didcomm/protocol/legacyconnection/states_test.go index dbdbcc8b83..0cd842b553 100644 --- a/pkg/didcomm/protocol/legacyconnection/states_test.go +++ b/pkg/didcomm/protocol/legacyconnection/states_test.go @@ -442,7 +442,7 @@ func TestCompletedState_Execute(t *testing.T) { invitation, err := createMockInvitation(pubKey, ctx) require.NoError(t, err) - connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.ID) + connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.RecipientKeys[0]) require.NoError(t, err) response := &Response{ @@ -625,7 +625,7 @@ func TestNewResponseFromRequest(t *testing.T) { request, err := createRequest(t, ctx) require.NoError(t, err) - request.Connection.DID = "invalid" + request.Connection.DID = "did:invalid" _, connRec, err := ctx.handleInboundRequest(request, &options{}, &connection.Record{}) require.Error(t, err) require.Contains(t, err.Error(), "resolve did doc from connection request") @@ -665,6 +665,35 @@ func TestNewResponseFromRequest(t *testing.T) { require.Nil(t, connRec) }) + t.Run("prepare connection signature get invitation", func(t *testing.T) { + ctx := getContext(t, &prov, transport.MediaTypeRFC0019EncryptedEnvelope) + + request, err := createRequest(t, ctx) + request.Thread.PID = "test" + + require.NoError(t, err) + _, connRec, err := ctx.handleInboundRequest(request, &options{}, &connection.Record{}) + require.Error(t, err) + + require.Contains(t, err.Error(), "get invitation for [invitationID=test]: data not found") + require.Nil(t, connRec) + }) + + t.Run("prepare connection signature get invitation", func(t *testing.T) { + invID := randomString() + ctx := getContext(t, &prov, transport.MediaTypeRFC0019EncryptedEnvelope) + + request, err := createRequest(t, ctx) + request.Thread.PID = invID + + require.NoError(t, err) + _, connRec, err := ctx.handleInboundRequest(request, &options{}, &connection.Record{}) + require.Error(t, err) + + require.Contains(t, err.Error(), fmt.Sprintf("get invitation for [invitationID=%s]: data not found", invID)) + 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) @@ -698,7 +727,7 @@ func TestNewResponseFromRequest(t *testing.T) { 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"}} + request := &Request{Connection: &Connection{DID: "did:sidetree:abc", DIDDoc: &diddoc.Doc{}}} _, _, err := ctx.handleInboundRequest(request, &options{}, &connection.Record{}) require.Error(t, err) require.Contains(t, err.Error(), "resolver error") @@ -736,7 +765,7 @@ func TestPrepareConnectionSignature(t *testing.T) { require.NoError(t, err) t.Run("prepare connection signature", func(t *testing.T) { - connectionSignature, err := ctx.prepareConnectionSignature(doc.DIDDocument, invitation.ID) + connectionSignature, err := ctx.prepareConnectionSignature(doc.DIDDocument, invitation.RecipientKeys[0]) require.NoError(t, err) require.NotNil(t, connectionSignature) sigData, err := base64.URLEncoding.DecodeString(connectionSignature.SignedData) @@ -756,7 +785,7 @@ func TestPrepareConnectionSignature(t *testing.T) { require.NoError(t, err) require.NotNil(t, didConnStore) - connectionSignature, err := ctx.prepareConnectionSignature(doc.DIDDocument, invitation.ID) + connectionSignature, err := ctx.prepareConnectionSignature(doc.DIDDocument, invitation.RecipientKeys[0]) require.NoError(t, err) require.NotNil(t, connectionSignature) sigData, err := base64.URLEncoding.DecodeString(connectionSignature.SignedData) @@ -767,33 +796,12 @@ func TestPrepareConnectionSignature(t *testing.T) { require.NoError(t, err) require.Equal(t, doc.DIDDocument.ID, sigDataConnection.DID) }) - t.Run("prepare connection signature get invitation", func(t *testing.T) { - connectionSignature, err := ctx.prepareConnectionSignature(doc.DIDDocument, "test") - require.Error(t, err) - require.Contains(t, err.Error(), "get invitation for [invitationID=test]: data not found") - require.Nil(t, connectionSignature) - }) - t.Run("prepare connection signature get invitation", func(t *testing.T) { - invID := randomString() - inv := &Invitation{ - Type: InvitationMsgType, - ID: invID, - DID: "test", - } - err := ctx.connectionRecorder.SaveInvitation(invitation.ID, invitation) - require.NoError(t, err) - connectionSignature, err := ctx.prepareConnectionSignature(doc.DIDDocument, inv.ID) - require.Error(t, err) - require.Contains(t, err.Error(), - fmt.Sprintf("get invitation for [invitationID=%s]: data not found", invID)) - require.Nil(t, connectionSignature) - }) t.Run("prepare connection signature error", func(t *testing.T) { ctx2 := ctx ctx2.crypto = &mockcrypto.Crypto{SignErr: errors.New("sign error")} newDIDDoc := mockdiddoc.GetMockDIDDoc(t, false) - connectionSignature, err := ctx2.prepareConnectionSignature(newDIDDoc, invitation.ID) + connectionSignature, err := ctx2.prepareConnectionSignature(newDIDDoc, invitation.RecipientKeys[0]) require.Error(t, err) require.Contains(t, err.Error(), "sign error") require.Nil(t, connectionSignature) @@ -814,7 +822,7 @@ func TestVerifySignature(t *testing.T) { require.NoError(t, err) t.Run("signature verified", func(t *testing.T) { - connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.ID) + connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.RecipientKeys[0]) require.NoError(t, err) con, err := ctx.verifySignature(connectionSignature, invitation.RecipientKeys[0]) require.NoError(t, err) @@ -828,7 +836,7 @@ func TestVerifySignature(t *testing.T) { require.Nil(t, con) }) t.Run("decode signature data error", func(t *testing.T) { - connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.ID) + connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.RecipientKeys[0]) require.NoError(t, err) connectionSignature.SignedData = "invalid-signed-data" @@ -838,7 +846,7 @@ func TestVerifySignature(t *testing.T) { require.Nil(t, con) }) t.Run("decode signature error", func(t *testing.T) { - connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.ID) + connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.RecipientKeys[0]) require.NoError(t, err) connectionSignature.Signature = "invalid-signature" @@ -848,7 +856,7 @@ func TestVerifySignature(t *testing.T) { require.Nil(t, con) }) t.Run("decode verification key error", func(t *testing.T) { - connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.ID) + connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.RecipientKeys[0]) require.NoError(t, err) con, err := ctx.verifySignature(connectionSignature, "invalid-key") @@ -857,7 +865,7 @@ func TestVerifySignature(t *testing.T) { require.Nil(t, con) }) t.Run("verify signature error", func(t *testing.T) { - connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.ID) + connectionSignature, err := ctx.prepareConnectionSignature(newDIDDoc, invitation.RecipientKeys[0]) require.NoError(t, err) // generate different key and assign it to signature verification key @@ -970,14 +978,6 @@ func TestResolveDIDDocFromConnection(t *testing.T) { require.Contains(t, err.Error(), "failed to resolve public did") }) - t.Run(fmt.Sprintf("failure - can't parse did with media type profile: %s", mtp), func(t *testing.T) { - ctx := getContext(t, &prov, mtp) - - _, err := ctx.resolveDidDocFromConnection(&Connection{DID: "blah blah"}) - require.Error(t, err) - require.Contains(t, err.Error(), "failed to parse did") - }) - t.Run(fmt.Sprintf("failure - missing attachment for private did with media type profile: %s", mtp), func(t *testing.T) { ctx := getContext(t, &prov, mtp) @@ -1450,7 +1450,12 @@ func createResponse(request *Request, ctx *context) (*Response, error) { return nil, err } - connectionSignature, err := ctx.prepareConnectionSignature(doc.DIDDocument, request.Thread.PID) + verKey, err := ctx.getVerKey(request.Thread.PID) + if err != nil { + return nil, err + } + + connectionSignature, err := ctx.prepareConnectionSignature(doc.DIDDocument, verKey) if err != nil { return nil, err } diff --git a/pkg/didcomm/transport/http/inbound.go b/pkg/didcomm/transport/http/inbound.go index 1330c9db75..93128b387d 100644 --- a/pkg/didcomm/transport/http/inbound.go +++ b/pkg/didcomm/transport/http/inbound.go @@ -100,7 +100,7 @@ func validateHTTPMethod(w http.ResponseWriter, r *http.Request) bool { ct := r.Header.Get("Content-type") - if ct != commContentType { + if ct != commContentType && ct != commContentTypeLegacy { http.Error(w, fmt.Sprintf("Unsupported Content-type \"%s\"", ct), http.StatusUnsupportedMediaType) return false } diff --git a/pkg/didcomm/transport/http/inbound_test.go b/pkg/didcomm/transport/http/inbound_test.go index 08191d854b..4dda6c6897 100644 --- a/pkg/didcomm/transport/http/inbound_test.go +++ b/pkg/didcomm/transport/http/inbound_test.go @@ -107,6 +107,12 @@ func TestInboundHandler(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusBadRequest, rs.StatusCode) + rs, err = client.Post(serverURL+"/", commContentTypeLegacy, nil) + require.NoError(t, err) + err = rs.Body.Close() + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, rs.StatusCode) + // finally test successful POST requests data := "success" @@ -117,6 +123,13 @@ func TestInboundHandler(t *testing.T) { require.NotNil(t, resp) require.Equal(t, http.StatusAccepted, resp.StatusCode) + resp, err = client.Post(serverURL+"/", commContentTypeLegacy, bytes.NewBuffer([]byte(data))) + require.NoError(t, err) + err = resp.Body.Close() + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, http.StatusAccepted, resp.StatusCode) + // test unpack error mockPackager.UnpackValue = nil mockPackager.UnpackErr = fmt.Errorf("unpack error") @@ -128,6 +141,15 @@ func TestInboundHandler(t *testing.T) { require.NoError(t, err) require.Contains(t, string(body), "failed to unpack msg") require.NoError(t, resp.Body.Close()) + + resp, err = client.Post(serverURL+"/", commContentTypeLegacy, bytes.NewBuffer([]byte(data))) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, http.StatusInternalServerError, resp.StatusCode) + body, err = ioutil.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "failed to unpack msg") + require.NoError(t, resp.Body.Close()) } func TestInboundTransport(t *testing.T) { @@ -203,6 +225,14 @@ func TestInboundTransport(t *testing.T) { err = resp.Body.Close() require.NoError(t, err) + resp, err = client.Post("http://localhost:26605", commContentTypeLegacy, bytes.NewBuffer([]byte("success"))) + require.NoError(t, err) + require.Equal(t, http.StatusAccepted, resp.StatusCode) + require.NotNil(t, resp) + + err = resp.Body.Close() + require.NoError(t, err) + // stop server err = inbound.Stop() require.NoError(t, err) @@ -210,6 +240,9 @@ func TestInboundTransport(t *testing.T) { // try after server stop _, err = client.Post("http://localhost:26605", commContentType, bytes.NewBuffer([]byte("success"))) // nolint require.Error(t, err) + + _, err = client.Post("http://localhost:26605", commContentTypeLegacy, bytes.NewBuffer([]byte("success"))) // nolint + require.Error(t, err) }) } diff --git a/pkg/didcomm/transport/http/outbound.go b/pkg/didcomm/transport/http/outbound.go index 5bea1ab2b6..2492c68bb7 100644 --- a/pkg/didcomm/transport/http/outbound.go +++ b/pkg/didcomm/transport/http/outbound.go @@ -22,8 +22,9 @@ import ( //go:generate testdata/scripts/openssl_env.sh testdata/scripts/generate_test_keys.sh const ( - commContentType = "application/didcomm-envelope-enc" - httpScheme = "http" + commContentType = "application/didcomm-envelope-enc" + commContentTypeLegacy = "application/ssi-agent-wire" + httpScheme = "http" ) // outboundCommHTTPOpts holds options for the HTTP transport implementation of CommTransport diff --git a/pkg/doc/did/doc.go b/pkg/doc/did/doc.go index f41801483d..375a759134 100644 --- a/pkg/doc/did/doc.go +++ b/pkg/doc/did/doc.go @@ -483,8 +483,13 @@ func ParseDocument(data []byte) (*Doc, error) { } // Interop: handle legacy did docs that incorrectly indicate they use the new format - // aca-py issue: https://github.com/hyperledger/aries-cloudagent-python/issues/1048 - if doACAPYInterop && requiresLegacyHandling(raw) { + // aca-py and vcx issue: https://github.com/hyperledger/aries-cloudagent-python/issues/1048 + var serviceType string + if len(raw.Service) > 0 { + serviceType, _ = raw.Service[0]["type"].(string) //nolint: errcheck + } + + if (doACAPYInterop || serviceType == "IndyAgent") && requiresLegacyHandling(raw) { raw.Context = []string{contextV011} } else { // validate did document diff --git a/pkg/doc/did/legacy.go b/pkg/doc/did/legacy.go new file mode 100644 index 0000000000..57ff0444a4 --- /dev/null +++ b/pkg/doc/did/legacy.go @@ -0,0 +1,120 @@ +/* +Copyright Avast Software. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package did + +import ( + "fmt" +) + +// ToLegacyRawDoc converts document to raw doc. +func (doc *Doc) ToLegacyRawDoc() (interface{}, error) { + context := ContextV1Old + + publicKey, err := populateRawVM(context, doc.ID, doc.processingMeta.baseURI, doc.VerificationMethod) + if err != nil { + return nil, fmt.Errorf("JSON unmarshalling of Legacy Verification Method failed: %w", err) + } + + auths := populateRawVerificationLegacy(doc.processingMeta.baseURI, doc.ID, doc.Authentication) + + assertionMethods := populateRawVerificationLegacy(doc.processingMeta.baseURI, doc.ID, + doc.AssertionMethod) + + capabilityDelegations := populateRawVerificationLegacy(doc.processingMeta.baseURI, doc.ID, + doc.CapabilityDelegation) + + capabilityInvocations := populateRawVerificationLegacy(doc.processingMeta.baseURI, doc.ID, + doc.CapabilityInvocation) + + keyAgreements := populateRawVerificationLegacy(doc.processingMeta.baseURI, doc.ID, doc.KeyAgreement) + + services := populateRawServicesLegacy(doc.Service, doc.ID, doc.processingMeta.baseURI) + + raw := &rawDoc{ + Context: context, ID: doc.ID, PublicKey: publicKey, + Authentication: auths, AssertionMethod: assertionMethods, CapabilityDelegation: capabilityDelegations, + CapabilityInvocation: capabilityInvocations, KeyAgreement: keyAgreements, + Service: services, Created: doc.Created, + Proof: populateRawProofs(context, doc.ID, doc.processingMeta.baseURI, doc.Proof), Updated: doc.Updated, + } + + if doc.processingMeta.baseURI != "" { + raw.Context = contextWithBase(doc) + } + + return raw, nil +} + +func populateRawVerificationLegacy(baseURI, didID string, verifications []Verification) []interface{} { + var rawVerifications []interface{} + + for _, v := range verifications { + keyRef := map[string]string{} + + if v.VerificationMethod.relativeURL { + keyRef["publicKey"] = makeRelativeDIDURL(v.VerificationMethod.ID, baseURI, didID) + } else { + keyRef["publicKey"] = v.VerificationMethod.ID + } + + keyRef["type"] = v.VerificationMethod.Type + + rawVerifications = append(rawVerifications, keyRef) + } + + return rawVerifications +} + +func populateRawServicesLegacy(services []Service, didID, baseURI string) []map[string]interface{} { + var rawServices []map[string]interface{} + + for i := range services { + rawService := make(map[string]interface{}) + + for k, v := range services[i].Properties { + rawService[k] = v + } + + routingKeys := make([]string, 0) + + for _, v := range services[i].RoutingKeys { + if services[i].routingKeysRelativeURL[v] { + routingKeys = append(routingKeys, makeRelativeDIDURL(v, baseURI, didID)) + continue + } + + routingKeys = append(routingKeys, v) + } + + recipientKeys := make([]string, 0) + + for _, v := range services[i].RecipientKeys { + if services[i].recipientKeysRelativeURL[v] { + recipientKeys = append(recipientKeys, makeRelativeDIDURL(v, baseURI, didID)) + continue + } + + recipientKeys = append(recipientKeys, v) + } + + rawService[jsonldID] = services[i].ID + if services[i].relativeURL { + rawService[jsonldID] = makeRelativeDIDURL(services[i].ID, baseURI, didID) + } + + uri, _ := services[i].ServiceEndpoint.URI() //nolint: errcheck + + rawService[jsonldType] = services[i].Type + rawService[jsonldServicePoint] = uri + rawService[jsonldRecipientKeys] = recipientKeys + rawService[jsonldRoutingKeys] = routingKeys + rawService[jsonldPriority] = services[i].Priority + + rawServices = append(rawServices, rawService) + } + + return rawServices +} diff --git a/pkg/doc/did/legacy_test.go b/pkg/doc/did/legacy_test.go new file mode 100644 index 0000000000..afeeb7799d --- /dev/null +++ b/pkg/doc/did/legacy_test.go @@ -0,0 +1,44 @@ +/* +Copyright Avast Software. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package did + +import ( + "github.com/btcsuite/btcutil/base58" + "github.com/stretchr/testify/require" + "testing" +) + +func TestToLegacyRawDoc(t *testing.T) { + // setup -> create Document from json byte data + doc, err := ParseDocument([]byte(validDocV011)) + require.NoError(t, err) + require.NotEmpty(t, doc) + + // convert Document to json byte data + rawDoc, err := doc.ToLegacyRawDoc() + require.NoError(t, err) + require.NotEmpty(t, rawDoc) + + require.Equal(t, rawDoc.ID, doc.ID) + + require.Equal(t, rawDoc.Context, ContextV1Old) + + require.Equal(t, rawDoc.PublicKey[0]["type"], doc.VerificationMethod[0].Type) + require.Equal(t, rawDoc.PublicKey[0]["publicKeyBase58"], base58.Encode(doc.VerificationMethod[0].Value)) + require.Equal(t, rawDoc.PublicKey[1]["type"], doc.VerificationMethod[1].Type) + require.Equal(t, rawDoc.PublicKey[1]["publicKeyBase58"], base58.Encode(doc.VerificationMethod[1].Value)) + + require.Equal(t, rawDoc.Authentication[0].(map[string]string)["publicKey"], doc.Authentication[0].VerificationMethod.ID) + require.Equal(t, rawDoc.Authentication[0].(map[string]string)["type"], doc.Authentication[0].VerificationMethod.Type) + require.Equal(t, rawDoc.Authentication[1].(map[string]string)["publicKey"], doc.Authentication[1].VerificationMethod.ID) + require.Equal(t, rawDoc.Authentication[1].(map[string]string)["type"], doc.Authentication[1].VerificationMethod.Type) + + require.Equal(t, rawDoc.PublicKey[0]["id"], doc.VerificationMethod[0].ID) + require.Equal(t, rawDoc.Service[0]["id"], doc.Service[0].ID) + uri, err := doc.Service[0].ServiceEndpoint.URI() + require.NoError(t, err) + require.Equal(t, rawDoc.Service[0]["serviceEndpoint"], uri) +} diff --git a/pkg/internal/didkeyutil/util.go b/pkg/internal/didkeyutil/util.go index a0ae78199d..c7aa43d318 100644 --- a/pkg/internal/didkeyutil/util.go +++ b/pkg/internal/didkeyutil/util.go @@ -49,3 +49,19 @@ func ConvertBase58KeysToDIDKeys(keys []string) []string { return didKeys } + +// ConvertDIDKeysToBase58Keys converts base58 keys array to did:key keys array. +func ConvertDIDKeysToBase58Keys(keys []string) []string { + var base58Keys []string + for _, key := range keys { + if strings.HasPrefix(key, "did:key:") { + rawKey, _ := fingerprint.PubKeyFromDIDKey(key) //nolint: errcheck + + base58Keys = append(base58Keys, base58.Encode(rawKey)) + } else { + base58Keys = append(base58Keys, key) + } + } + + return base58Keys +} diff --git a/pkg/internal/didkeyutil/util_test.go b/pkg/internal/didkeyutil/util_test.go index 0b27fdbc02..86096a06ab 100644 --- a/pkg/internal/didkeyutil/util_test.go +++ b/pkg/internal/didkeyutil/util_test.go @@ -69,3 +69,48 @@ func TestConvertBase58KeysToDIDKeys(t *testing.T) { } }) } + +func TestConvertDIDKeysToBase58Keys(t *testing.T) { + t.Run("no keys given", func(t *testing.T) { + recipientKeys := ConvertDIDKeysToBase58Keys(nil) + require.Nil(t, recipientKeys) + }) + + t.Run("some keys are converted", func(t *testing.T) { + inKeys := []string{ + "did:key:z6MkjtX1C5tGbsNxcGBdCnkfzPw4pHq3fuufgFNkBpFtviAL", + "#key1", + "did:key:z6MkkFUoo38XGcBVojEhiGum4EV58DCCdGnigLHqvFMWiz3H", + "did:key:z6MkerVcrLfHZ9SajtxAkkir2wUwsQ9hg85oQfU62pyMtgsQ", + "/path#fragment", + "did:key:z6MkoG3wcwpJBS7GpzJSs1rPuTgiA7tsUV2FyVqszD1kWNNJ", + "did:key:z6MkuusSJ7WsP58DcpvU7hqoiYtzkxwHb5nrE9xLUj76PK9n", + "?query=value", + "did:key:z6MktheMCSkicACB2D4nP3y2JX8i1ZofyYvuKtMK5Zs78ewa", + "", + "@!~unexpected data~!@", + } + + expectedKeys := []string{ + "6SFxbqdqGKtVVmLvXDnq9JP4ziZCG2fJzETpMYHt1VNx", + "#key1", + "6oDmCnt5w4h2hEQ12hwvD8w5JdvMDPYMzKNv5yPVomFu", + "QEaG6QrDbx7dQ7U5Bm1Bqvx3psrGEqSieZACZ1LyU62", + "/path#fragment", + "9onu2hZrqtcoiVTkBStZ4N8iLYd24bmuHUvx9w3jb9av", + "GTcPhsGS3XdkWL5mS8sxsTLzwPfSBCYVY93QeT95U6NQ", + "?query=value", + "FFPJcCWHGchhuiE5hV1BTRaiBzXpZfgYdsSPFHu6DSAC", + "", + "@!~unexpected data~!@", + } + + outKeys := ConvertDIDKeysToBase58Keys(inKeys) + + require.Equal(t, len(expectedKeys), len(outKeys)) + + for i := range outKeys { + require.Equal(t, expectedKeys[i], outKeys[i]) + } + }) +} diff --git a/pkg/store/connection/connection_lookup.go b/pkg/store/connection/connection_lookup.go index be5d07fb85..5b8d2e03ec 100644 --- a/pkg/store/connection/connection_lookup.go +++ b/pkg/store/connection/connection_lookup.go @@ -55,24 +55,25 @@ type DIDRotationRecord struct { // Record contain info about did exchange connection. type Record struct { - ConnectionID string - State string - ThreadID string - ParentThreadID string - TheirLabel string - TheirDID string - MyDID string - ServiceEndPoint model.Endpoint // ServiceEndPoint is 'their' DIDComm service endpoint. - RecipientKeys []string // RecipientKeys holds 'their' DIDComm recipient keys. - RoutingKeys []string // RoutingKeys holds 'their' DIDComm routing keys. - InvitationID string - InvitationDID string - Implicit bool - Namespace string - MediaTypeProfiles []string - DIDCommVersion didcomm.Version - PeerDIDInitialState string - MyDIDRotation *DIDRotationRecord `json:"myDIDRotation,omitempty"` + ConnectionID string + State string + ThreadID string + ParentThreadID string + TheirLabel string + TheirDID string + MyDID string + ServiceEndPoint model.Endpoint // ServiceEndPoint is 'their' DIDComm service endpoint. + RecipientKeys []string // RecipientKeys holds 'their' DIDComm recipient keys. + RoutingKeys []string // RoutingKeys holds 'their' DIDComm routing keys. + InvitationID string + InvitationDID string + InvitationRecipientKeys []string `json:"invitationRecipientKeys,omitempty"` // InvitationRecipientKeys holds keys generated for invitation // nolint: lll + Implicit bool + Namespace string + MediaTypeProfiles []string + DIDCommVersion didcomm.Version + PeerDIDInitialState string + MyDIDRotation *DIDRotationRecord `json:"myDIDRotation,omitempty"` } // NewLookup returns new connection lookup instance.