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..9bb8d8aa75 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,80 @@ 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) toLegacyJSONBytes() ([]byte, error) { + if con.DIDDoc == nil { + return nil, fmt.Errorf("DIDDoc field cannot be empty") + } + + 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 parseLegacyJSONBytes(data []byte) (*Connection, error) { + connRaw := &connectionRaw{} + + err := json.Unmarshal(data, &connRaw) + if err != nil { + return nil, fmt.Errorf("JSON umarshalling of connection data bytes failed: %w", err) + } else if connRaw.DIDDoc == nil { + return nil, errors.New("connection DIDDoc field is missed") + } + + docRaw, err := json.Marshal(connRaw.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: connRaw.DID, + DIDDoc: doc, + }, nil +} diff --git a/pkg/didcomm/protocol/legacyconnection/models_test.go b/pkg/didcomm/protocol/legacyconnection/models_test.go new file mode 100644 index 0000000000..29bbc398d9 --- /dev/null +++ b/pkg/didcomm/protocol/legacyconnection/models_test.go @@ -0,0 +1,110 @@ +/* +Copyright Avast Software. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package legacyconnection + +import ( + _ "embed" + "encoding/json" + "testing" + + "github.com/btcsuite/btcutil/base58" + "github.com/stretchr/testify/require" + + "github.com/hyperledger/aries-framework-go/pkg/doc/did" +) + +//nolint:gochecknoglobals +var ( + //go:embed testdata/valid_conn_data.jsonld + validConnectionData string +) + +// nolint:lll +func TestJSONConversion(t *testing.T) { + con, err := parseLegacyJSONBytes([]byte(validConnectionData)) + require.NoError(t, err) + require.NotEmpty(t, con) + + require.Equal(t, con.DID, "did:example:21tDAKCERh95uGgKbJNHYp") + require.Equal(t, con.DIDDoc.ID, "did:example:21tDAKCERh95uGgKbJNHYp") + require.Contains(t, con.DIDDoc.Context, "https://w3id.org/did/v0.11") + require.Equal(t, con.DIDDoc.AlsoKnownAs[0], "did:example:123") + require.Equal(t, con.DIDDoc.VerificationMethod[0].Type, "Secp256k1VerificationKey2018") + require.Equal(t, base58.Encode(con.DIDDoc.VerificationMethod[0].Value), "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV") + require.Equal(t, con.DIDDoc.Authentication[0].VerificationMethod.Type, "Secp256k1VerificationKey2018") + require.Equal(t, base58.Encode(con.DIDDoc.Authentication[0].VerificationMethod.Value), "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV") + require.Equal(t, con.DIDDoc.Service[0].Type, "IndyAgent") + uri, err := con.DIDDoc.Service[0].ServiceEndpoint.URI() + require.NoError(t, err) + require.Contains(t, uri, "https://agent.example.com/") + require.Equal(t, con.DIDDoc.Service[0].RecipientKeys, []string{"did:example:123456789abcdefghi#key2"}) + require.Equal(t, con.DIDDoc.Service[0].RoutingKeys, []string{"did:example:123456789abcdefghi#key2"}) + + conBytes, err := con.toLegacyJSONBytes() + require.NoError(t, err) + require.NotEmpty(t, conBytes) + + rawString := string(conBytes) + + require.Contains(t, rawString, "\"DID\":\"did:example:21tDAKCERh95uGgKbJNHYp\"") + require.Contains(t, rawString, "\"@context\":\"https://w3id.org/did/v1\"") + require.Contains(t, rawString, "\"controller\":\"did:example:123456789abcdefghi\",\"id\":\"did:example:123456789abcdefghi#keys-1\",\"publicKeyBase58\":\"H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV\",\"type\":\"Secp256k1VerificationKey2018\"") + require.Contains(t, rawString, "\"authentication\":[{\"publicKey\":\"did:example:123456789abcdefghi#keys-1\",\"type\":\"Secp256k1VerificationKey2018\"},{\"publicKey\":\"did:example:123456789abcdefghs#key3\",\"type\":\"RsaVerificationKey2018\"}]") + require.Contains(t, rawString, "\"service\":[{\"id\":\"did:example:123456789abcdefghi#did-communication\",\"priority\":0,\"recipientKeys\":[\"did:example:123456789abcdefghi#key2\"],\"routingKeys\":[\"did:example:123456789abcdefghi#key2\"],\"serviceEndpoint\":\"https://agent.example.com/\",\"type\":\"IndyAgent\"}]") +} + +func TestToLegacyJSONBytes(t *testing.T) { + con := &Connection{ + DID: "did:example:21tDAKCERh95uGgKbJNHYp", + } + + // Empty DIDDoc data + legacyDoc, err := con.toLegacyJSONBytes() + require.ErrorContains(t, err, "DIDDoc field cannot be empty") + require.Empty(t, legacyDoc) + + // Success + + con.DIDDoc = &did.Doc{ + Context: "https://w3id.org/did/v0.11", + ID: "did:example:21tDAKCERh95uGgKbJNHYp", + } + legacyDoc, err = con.toLegacyJSONBytes() + + require.NoError(t, err) + require.NotEmpty(t, legacyDoc) + require.Contains(t, string(legacyDoc), "\"DID\":\"did:example:21tDAKCERh95uGgKbJNHYp\"") + require.Contains(t, string(legacyDoc), "\"@context\":\"https://w3id.org/did/v1\"") +} + +func TestParseJSONBytes(t *testing.T) { + // Nil payload + conRaw, err := parseLegacyJSONBytes(nil) + require.ErrorContains(t, err, "JSON umarshalling of connection data bytes failed") + require.Empty(t, conRaw) + + // Empty payload + conRaw, err = parseLegacyJSONBytes([]byte{}) + require.ErrorContains(t, err, "JSON umarshalling of connection data bytes failed") + require.Empty(t, conRaw) + + // Empty DIDDoc + docBytes, err := json.Marshal(Connection{ + DID: "did:example:21tDAKCERh95uGgKbJNHYp", + }) + require.NoError(t, err) + require.NotEmpty(t, docBytes) + + conRaw, err = parseLegacyJSONBytes(docBytes) + require.ErrorContains(t, err, "connection DIDDoc field is missed") + require.Empty(t, conRaw) + + // Success + con, err := parseLegacyJSONBytes([]byte(validConnectionData)) + require.NoError(t, err) + require.NotEmpty(t, con) +} diff --git a/pkg/didcomm/protocol/legacyconnection/service.go b/pkg/didcomm/protocol/legacyconnection/service.go index e30296cd2c..afff9c5c7d 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,13 @@ 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 and invitation recipient key"+ + " on connection request with @id=%s", request.ID) + } } if request.Connection == nil { @@ -698,14 +709,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..9998572cf6 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,12 +1367,12 @@ 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) }) } -func TestAcceptExchangeRequest(t *testing.T) { +func TestAcceptConnectionRequest(t *testing.T) { sp := mockstorage.NewMockStoreProvider() k := newKMS(t, sp) ctx := &context{ @@ -1444,7 +1444,82 @@ func TestAcceptExchangeRequest(t *testing.T) { } } -func TestAcceptExchangeRequestWithPublicDID(t *testing.T) { +func TestAcceptConnectionRequestWithoutParentThreadID(t *testing.T) { + sp := mockstorage.NewMockStoreProvider() + k := newKMS(t, sp) + ctx := &context{ + kms: k, + keyType: kms.ED25519Type, + keyAgreementType: kms.X25519ECDHKWType, + } + + svc, err := New(&protocol.MockProvider{ + StoreProvider: sp, + ServiceMap: map[string]interface{}{ + mediator.Coordination: &mockroute.MockMediatorSvc{}, + }, + CustomKMS: k, + KeyTypeValue: ctx.keyType, + KeyAgreementTypeValue: ctx.keyAgreementType, + }) + require.NoError(t, err) + + actionCh := make(chan service.DIDCommAction, 10) + err = svc.RegisterActionEvent(actionCh) + require.NoError(t, err) + + _, verPubKey, err := ctx.kms.CreateAndExportPubKeyBytes(kms.ED25519Type) + require.NoError(t, err) + + invitation := &Invitation{ + Type: InvitationMsgType, + ID: randomString(), + Label: "Bob", + RecipientKeys: []string{base58.Encode(verPubKey)}, + ServiceEndpoint: "http://alice.agent.example.com:8081", + } + + err = svc.connectionRecorder.SaveInvitation(invitation.ID, invitation) + require.NoError(t, err) + + go func() { + for e := range actionCh { + prop, ok := e.Properties.(event) + require.True(t, ok, "Failed to cast the event properties to service.Event") + require.NoError(t, svc.AcceptConnectionRequest(prop.ConnectionID(), "", "", nil)) + } + }() + + statusCh := make(chan service.StateMsg, 10) + err = svc.RegisterMsgEvent(statusCh) + require.NoError(t, err) + + done := make(chan struct{}) + + go func() { + for e := range statusCh { + if e.Type == service.PostState && e.StateID == StateIDResponded { + done <- struct{}{} + } + } + }() + + props := map[string]interface{}{} + props[InvitationRecipientKey] = invitation.RecipientKeys[0] + c := service.NewDIDCommContext("", "", props) + + _, err = svc.HandleInbound(generateRequestMsgPayload(t, &protocol.MockProvider{ + StoreProvider: mockstorage.NewMockStoreProvider(), + }, randomString(), ""), c) + require.NoError(t, err, "missing parent thread ID and invitation recipient key on connection request") + select { + case <-done: + case <-time.After(5 * time.Second): + require.Fail(t, "tests are not validated") + } +} + +func TestAcceptConnectionRequestWithPublicDID(t *testing.T) { sp := mockstorage.NewMockStoreProvider() k := newKMS(t, sp) ctx := &context{ diff --git a/pkg/didcomm/protocol/legacyconnection/states.go b/pkg/didcomm/protocol/legacyconnection/states.go index 782c38e9cb..b20fe35c7a 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" @@ -316,6 +316,7 @@ func (ctx *context) createConnectionRequest(destination *service.Destination, la }, connRec, nil } +// nolint:gocyclo,funlen func (ctx *context) handleInboundRequest(request *Request, options *options, connRec *connectionstore.Record) (stateAction, *connectionstore.Record, error) { logger.Debugf("handling request: %#v", request) @@ -346,8 +347,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 +375,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 +391,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.toLegacyJSONBytes() if err != nil { return nil, fmt.Errorf("failed to marshal connection : %w", err) } @@ -399,11 +409,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 +549,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 +627,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 +655,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) } @@ -759,9 +764,8 @@ func (ctx *context) verifySignature(connSignature *ConnectionSignature, recipien } connBytes := sigData[timestampLength:] - conn := &Connection{} - err = json.Unmarshal(connBytes, conn) + conn, err := parseLegacyJSONBytes(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/protocol/legacyconnection/testdata/valid_conn_data.jsonld b/pkg/didcomm/protocol/legacyconnection/testdata/valid_conn_data.jsonld new file mode 100644 index 0000000000..a8084becf0 --- /dev/null +++ b/pkg/didcomm/protocol/legacyconnection/testdata/valid_conn_data.jsonld @@ -0,0 +1,47 @@ +{ + "DID": "did:example:21tDAKCERh95uGgKbJNHYp", + "DIDDoc": { + "@context": "https://w3id.org/did/v1", + "id": "did:example:21tDAKCERh95uGgKbJNHYp", + "alsoKnownAs": [ + "did:example:123" + ], + "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#did-communication", + "type": "IndyAgent", + "serviceEndpoint": "https://agent.example.com/", + "priority" : 0, + "recipientKeys" : ["did:example:123456789abcdefghi#key2"], + "routingKeys" : ["did:example:123456789abcdefghi#key2"] + } + ], + "created": "2002-10-10T17:00:00Z" + } +} \ No newline at end of file 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..8e04801332 100644 --- a/pkg/didcomm/transport/http/inbound_test.go +++ b/pkg/didcomm/transport/http/inbound_test.go @@ -100,34 +100,40 @@ func TestInboundHandler(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusUnsupportedMediaType, rs.StatusCode) - // test with nil body .. - rs, err = client.Post(serverURL+"/", commContentType, 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 + contentTypes := []string{commContentType, commContentTypeLegacy} data := "success" - resp, err := client.Post(serverURL+"/", commContentType, 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) + for _, contentType := range contentTypes { + // test with nil body .. + resp, err := client.Post(serverURL+"/", contentType, nil) + require.NoError(t, err) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.NoError(t, resp.Body.Close()) + + // test successful POST requests + resp, err = client.Post(serverURL+"/", contentType, 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") - resp, err = client.Post(serverURL+"/", commContentType, 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()) + + for _, contentType := range contentTypes { + resp, err := client.Post(serverURL+"/", contentType, 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) { @@ -193,23 +199,31 @@ func TestInboundTransport(t *testing.T) { err = inbound.Start(&mockProvider{packagerValue: mockPackager}) require.NoError(t, err) require.NoError(t, listenFor("localhost:26605", time.Second)) - // invoke a endpoint + + contentTypes := []string{commContentType, commContentTypeLegacy} client := http.Client{} - resp, err := client.Post("http://localhost:26605", commContentType, 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) + for _, contentType := range contentTypes { + // invoke a endpoint + var resp *http.Response + resp, err = client.Post("http://localhost:26605", contentType, 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) // try after server stop - _, err = client.Post("http://localhost:26605", commContentType, bytes.NewBuffer([]byte("success"))) // nolint - require.Error(t, err) + for _, contentType := range contentTypes { + _, err = client.Post("http://localhost:26605", contentType, 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..1cbd94ccbe 100644 --- a/pkg/doc/did/doc.go +++ b/pkg/doc/did/doc.go @@ -60,6 +60,9 @@ const ( jsonldPublicKeyHex = "publicKeyHex" jsonldPublicKeyPem = "publicKeyPem" jsonldPublicKeyjwk = "publicKeyJwk" + + // service type that needed for v011 did-doc resolution. + legacyServiceType = "IndyAgent" ) var ( @@ -472,7 +475,7 @@ func (doc *Doc) UnmarshalJSON(data []byte) error { } // ParseDocument creates an instance of DIDDocument by reading a JSON document from bytes. -func ParseDocument(data []byte) (*Doc, error) { +func ParseDocument(data []byte) (*Doc, error) { // nolint:funlen,gocyclo raw := &rawDoc{} err := json.Unmarshal(data, &raw) @@ -483,8 +486,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 == legacyServiceType) && 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..718e5dcab2 --- /dev/null +++ b/pkg/doc/did/legacy_test.go @@ -0,0 +1,48 @@ +/* +Copyright Avast Software. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package did + +import ( + "testing" + + "github.com/btcsuite/btcutil/base58" + "github.com/mitchellh/mapstructure" + "github.com/stretchr/testify/require" +) + +func TestToLegacyRawDoc(t *testing.T) { + doc, err := ParseDocument([]byte(validDocV011)) + require.NoError(t, err) + require.NotEmpty(t, doc) + + legacyRawDoc, err := doc.ToLegacyRawDoc() + require.NoError(t, err) + + rawDoc := &rawDoc{} + err = mapstructure.Decode(legacyRawDoc, rawDoc) + 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) //nolint: lll + 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) //nolint: lll + 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..3df10e079f 100644 --- a/pkg/internal/didkeyutil/util.go +++ b/pkg/internal/didkeyutil/util.go @@ -49,3 +49,20 @@ 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..64886c3c19 100644 --- a/pkg/store/connection/connection_lookup.go +++ b/pkg/store/connection/connection_lookup.go @@ -54,25 +54,27 @@ type DIDRotationRecord struct { } // Record contain info about did exchange connection. +// nolint:lll 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 + Implicit bool + Namespace string + MediaTypeProfiles []string + DIDCommVersion didcomm.Version + PeerDIDInitialState string + MyDIDRotation *DIDRotationRecord `json:"myDIDRotation,omitempty"` } // NewLookup returns new connection lookup instance.