From 447272a1f1e419a39a4e524e0def92793527c240 Mon Sep 17 00:00:00 2001 From: mcrakhman Date: Tue, 25 Apr 2023 09:54:12 +0200 Subject: [PATCH] Move receipt logic to any-sync, write tests --- coordinator/coordinatorclient/receipt.go | 85 ------------ coordinator/coordinatorproto/receipt.go | 100 ++++++++++++++ coordinator/coordinatorproto/receipt_test.go | 130 +++++++++++++++++++ 3 files changed, 230 insertions(+), 85 deletions(-) delete mode 100644 coordinator/coordinatorclient/receipt.go create mode 100644 coordinator/coordinatorproto/receipt.go create mode 100644 coordinator/coordinatorproto/receipt_test.go diff --git a/coordinator/coordinatorclient/receipt.go b/coordinator/coordinatorclient/receipt.go deleted file mode 100644 index 4a7f3162..00000000 --- a/coordinator/coordinatorclient/receipt.go +++ /dev/null @@ -1,85 +0,0 @@ -package coordinatorclient - -import ( - "bytes" - "context" - "errors" - "github.com/anytypeio/any-sync/commonspace/spacesyncproto" - "github.com/anytypeio/any-sync/coordinator/coordinatorproto" - "github.com/anytypeio/any-sync/net/peer" - "github.com/anytypeio/any-sync/nodeconf" - "github.com/anytypeio/any-sync/util/crypto" - "github.com/gogo/protobuf/proto" - "golang.org/x/exp/slices" - "time" -) - -var ( - errReceiptSignatureIncorrect = errors.New("receipt signature is incorrect") - errNoSuchCoordinatorNode = errors.New("no such control node") - errReceiptSpaceIdIncorrect = errors.New("receipt space id is incorrect") - errReceiptPeerIdIncorrect = errors.New("receipt peer id is incorrect") - errReceiptAccountIncorrect = errors.New("receipt account id is incorrect") - errReceiptExpired = errors.New("receipt is expired") -) - -func CheckReceipt(ctx context.Context, confService nodeconf.Service, request *spacesyncproto.SpacePushRequest) (err error) { - peerId, err := peer.CtxPeerId(ctx) - if err != nil { - return - } - accountIdentity, err := peer.CtxIdentity(ctx) - if err != nil { - return - } - credential := &coordinatorproto.SpaceReceiptWithSignature{} - err = proto.Unmarshal(request.GetCredential(), credential) - if err != nil { - return - } - payload := &coordinatorproto.SpaceReceipt{} - err = proto.Unmarshal(credential.GetSpaceReceiptPayload(), payload) - if err != nil { - return - } - if payload.GetSpaceId() != request.GetPayload().GetSpaceHeader().GetId() { - return errReceiptSpaceIdIncorrect - } - if payload.GetPeerId() != peerId { - return errReceiptPeerIdIncorrect - } - if !bytes.Equal(payload.GetAccountIdentity(), accountIdentity) { - return errReceiptAccountIncorrect - } - err = checkCoordinator( - confService, - payload.GetControlNodeIdentity(), - credential.GetSpaceReceiptPayload(), - credential.GetSignature()) - if err != nil { - return - } - if payload.GetValidUntil() < uint64(time.Now().Unix()) { - return errReceiptExpired - } - return -} - -func checkCoordinator(confService nodeconf.Service, identity []byte, payload, signature []byte) (err error) { - cooordinatorKey, err := crypto.UnmarshalEd25519PublicKey(identity) - if err != nil { - return - } - nodeTypes := confService.NodeTypes(cooordinatorKey.PeerId()) - if len(nodeTypes) == 0 || !slices.Contains(nodeTypes, nodeconf.NodeTypeCoordinator) { - return errNoSuchCoordinatorNode - } - res, err := cooordinatorKey.Verify(payload, signature) - if err != nil { - return - } - if !res { - return errReceiptSignatureIncorrect - } - return -} diff --git a/coordinator/coordinatorproto/receipt.go b/coordinator/coordinatorproto/receipt.go new file mode 100644 index 00000000..630eeffb --- /dev/null +++ b/coordinator/coordinatorproto/receipt.go @@ -0,0 +1,100 @@ +package coordinatorproto + +import ( + "bytes" + "errors" + "github.com/anytypeio/any-sync/util/crypto" + "github.com/gogo/protobuf/proto" + "golang.org/x/exp/slices" + "time" +) + +var ( + errReceiptSignatureIncorrect = errors.New("receipt signature is incorrect") + errNoSuchCoordinatorNode = errors.New("no such control node") + errReceiptSpaceIdIncorrect = errors.New("receipt space id is incorrect") + errReceiptPeerIdIncorrect = errors.New("receipt peer id is incorrect") + errReceiptAccountIncorrect = errors.New("receipt account is incorrect") + errReceiptExpired = errors.New("receipt is expired") +) + +func PrepareSpaceReceipt(spaceId, peerId string, validPeriod time.Duration, accountPubKey crypto.PubKey, nodeKey crypto.PrivKey) (signedReceipt *SpaceReceiptWithSignature, err error) { + marshalledAccount, err := accountPubKey.Marshall() + if err != nil { + return + } + marshalledNode, err := nodeKey.GetPublic().Marshall() + if err != nil { + return + } + receipt := &SpaceReceipt{ + SpaceId: spaceId, + PeerId: peerId, + AccountIdentity: marshalledAccount, + ControlNodeIdentity: marshalledNode, + ValidUntil: uint64(time.Now().Add(validPeriod).Unix()), + } + receiptData, err := receipt.Marshal() + if err != nil { + return + } + sign, err := nodeKey.Sign(receiptData) + if err != nil { + return + } + return &SpaceReceiptWithSignature{ + SpaceReceiptPayload: receiptData, + Signature: sign, + }, nil +} + +func CheckReceipt(peerId, spaceId string, accountIdentity []byte, coordinators []string, receipt *SpaceReceiptWithSignature) (err error) { + payload := &SpaceReceipt{} + err = proto.Unmarshal(receipt.GetSpaceReceiptPayload(), payload) + if err != nil { + return + } + if payload.SpaceId != spaceId { + return errReceiptSpaceIdIncorrect + } + if payload.PeerId != peerId { + return errReceiptPeerIdIncorrect + } + protoIdentity, err := crypto.UnmarshalEd25519PublicKeyProto(payload.AccountIdentity) + if err != nil { + return + } + if !bytes.Equal(protoIdentity.Storage(), accountIdentity) { + return errReceiptAccountIncorrect + } + err = checkCoordinator( + coordinators, + payload.ControlNodeIdentity, + receipt.GetSpaceReceiptPayload(), + receipt.GetSignature()) + if err != nil { + return + } + if payload.GetValidUntil() <= uint64(time.Now().Unix()) { + return errReceiptExpired + } + return +} + +func checkCoordinator(coordinators []string, identity []byte, payload, signature []byte) (err error) { + coordinatorKey, err := crypto.UnmarshalEd25519PublicKeyProto(identity) + if err != nil { + return + } + if !slices.Contains(coordinators, coordinatorKey.PeerId()) { + return errNoSuchCoordinatorNode + } + res, err := coordinatorKey.Verify(payload, signature) + if err != nil { + return + } + if !res { + return errReceiptSignatureIncorrect + } + return +} diff --git a/coordinator/coordinatorproto/receipt_test.go b/coordinator/coordinatorproto/receipt_test.go new file mode 100644 index 00000000..38ca6d75 --- /dev/null +++ b/coordinator/coordinatorproto/receipt_test.go @@ -0,0 +1,130 @@ +package coordinatorproto + +import ( + "context" + "crypto/rand" + "github.com/anytypeio/any-sync/util/crypto" + "github.com/gogo/protobuf/proto" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +type fixture struct { + coordinatorKey crypto.PrivKey + accountKey crypto.PubKey + accountIdentity []byte + ctx context.Context + originalReceipt *SpaceReceipt + signedReceipt *SpaceReceiptWithSignature + spaceId string + peerId string +} + +func newFixture(t *testing.T) *fixture { + coordinatorKey, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + signKey, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + signKeyRaw, err := signKey.GetPublic().Raw() + require.NoError(t, err) + return &fixture{ + spaceId: "spaceId", + peerId: "peerId", + accountIdentity: signKeyRaw, + coordinatorKey: coordinatorKey, + accountKey: signKey.GetPublic(), + } +} + +func (fx *fixture) prepareReceipt(t *testing.T, validPeriod time.Duration) { + var err error + fx.signedReceipt, err = PrepareSpaceReceipt(fx.spaceId, fx.peerId, validPeriod, fx.accountKey, fx.coordinatorKey) + require.NoError(t, err) + fx.originalReceipt = &SpaceReceipt{} + err = proto.Unmarshal(fx.signedReceipt.SpaceReceiptPayload, fx.originalReceipt) + require.NoError(t, err) + return +} + +func (fx *fixture) updateReceipt(t *testing.T, update func(t *testing.T, receipt *SpaceReceipt)) { + update(t, fx.originalReceipt) + marshalled, err := proto.Marshal(fx.originalReceipt) + require.NoError(t, err) + signature, err := fx.coordinatorKey.Sign(marshalled) + require.NoError(t, err) + fx.signedReceipt = &SpaceReceiptWithSignature{ + SpaceReceiptPayload: marshalled, + Signature: signature, + } +} + +func TestReceiptValid(t *testing.T) { + fx := newFixture(t) + fx.prepareReceipt(t, time.Second) + err := CheckReceipt(fx.peerId, fx.spaceId, fx.accountIdentity, []string{fx.coordinatorKey.GetPublic().PeerId()}, fx.signedReceipt) + require.NoError(t, err) +} + +func TestReceiptIncorrectSpaceId(t *testing.T) { + fx := newFixture(t) + fx.prepareReceipt(t, time.Second) + err := CheckReceipt(fx.peerId, "otherId", fx.accountIdentity, []string{fx.coordinatorKey.GetPublic().PeerId()}, fx.signedReceipt) + require.Error(t, errReceiptSpaceIdIncorrect, err) +} + +func TestReceiptIncorrectPeerId(t *testing.T) { + fx := newFixture(t) + fx.prepareReceipt(t, time.Second) + err := CheckReceipt("otherId", fx.spaceId, fx.accountIdentity, []string{fx.coordinatorKey.GetPublic().PeerId()}, fx.signedReceipt) + require.Error(t, errReceiptPeerIdIncorrect, err) +} + +func TestReceiptIncorrectAccountIdentity(t *testing.T) { + fx := newFixture(t) + fx.prepareReceipt(t, time.Second) + err := CheckReceipt(fx.peerId, fx.spaceId, []byte("some identity"), []string{fx.coordinatorKey.GetPublic().PeerId()}, fx.signedReceipt) + require.Error(t, errReceiptAccountIncorrect, err) +} + +func TestReceiptIncorrectCoordinatorKey(t *testing.T) { + fx := newFixture(t) + fx.prepareReceipt(t, time.Second) + + t.Run("random key payload", func(t *testing.T) { + fx.updateReceipt(t, func(t *testing.T, receipt *SpaceReceipt) { + receipt.AccountIdentity = []byte("some random stuff") + }) + err := CheckReceipt(fx.peerId, fx.spaceId, fx.accountIdentity, []string{fx.coordinatorKey.GetPublic().PeerId()}, fx.signedReceipt) + require.Error(t, err) + }) + t.Run("random incorrect key", func(t *testing.T) { + fx.updateReceipt(t, func(t *testing.T, receipt *SpaceReceipt) { + randomKey, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + keyBytes, err := randomKey.GetPublic().Marshall() + require.NoError(t, err) + receipt.AccountIdentity = keyBytes + }) + err := CheckReceipt(fx.peerId, fx.spaceId, fx.accountIdentity, []string{fx.coordinatorKey.GetPublic().PeerId()}, fx.signedReceipt) + require.Error(t, errNoSuchCoordinatorNode, err) + }) +} + +func TestReceiptIncorrectSignature(t *testing.T) { + fx := newFixture(t) + fx.prepareReceipt(t, time.Second) + fx.signedReceipt.Signature = []byte("random sig") + err := CheckReceipt(fx.peerId, fx.spaceId, fx.accountIdentity, []string{fx.coordinatorKey.GetPublic().PeerId()}, fx.signedReceipt) + require.Error(t, errReceiptSignatureIncorrect, err) +} + +func TestReceiptExpired(t *testing.T) { + fx := newFixture(t) + fx.prepareReceipt(t, time.Second) + fx.updateReceipt(t, func(t *testing.T, receipt *SpaceReceipt) { + receipt.ValidUntil = uint64(time.Now().Add(-time.Second).Unix()) + }) + err := CheckReceipt(fx.peerId, fx.spaceId, fx.accountIdentity, []string{fx.coordinatorKey.GetPublic().PeerId()}, fx.signedReceipt) + require.Error(t, errReceiptExpired, err) +}