diff --git a/net/peer/context.go b/net/peer/context.go index bf3b79f3..89626276 100644 --- a/net/peer/context.go +++ b/net/peer/context.go @@ -11,9 +11,13 @@ type contextKey uint const ( contextKeyPeerId contextKey = iota + contextKeyIdentity ) -var ErrPeerIdNotFoundInContext = errors.New("peer id not found in context") +var ( + ErrPeerIdNotFoundInContext = errors.New("peer id not found in context") + ErrIdentityNotFoundInContext = errors.New("identity not found in context") +) // CtxPeerId first tries to get peer id under our own key, but if it is not found tries to get through DRPC key func CtxPeerId(ctx context.Context) (string, error) { @@ -30,3 +34,16 @@ func CtxPeerId(ctx context.Context) (string, error) { func CtxWithPeerId(ctx context.Context, peerId string) context.Context { return context.WithValue(ctx, contextKeyPeerId, peerId) } + +// CtxIdentity returns identity from context +func CtxIdentity(ctx context.Context) ([]byte, error) { + if identity, ok := ctx.Value(contextKeyIdentity).([]byte); ok { + return identity, nil + } + return nil, ErrIdentityNotFoundInContext +} + +// CtxWithIdentity sets identity in the context +func CtxWithIdentity(ctx context.Context, identity []byte) context.Context { + return context.WithValue(ctx, contextKeyIdentity, identity) +} diff --git a/net/secureservice/credential.go b/net/secureservice/credential.go new file mode 100644 index 00000000..b9dffaf7 --- /dev/null +++ b/net/secureservice/credential.go @@ -0,0 +1,72 @@ +package secureservice + +import ( + "github.com/anytypeio/any-sync/commonspace/object/accountdata" + "github.com/anytypeio/any-sync/net/secureservice/handshake" + "github.com/anytypeio/any-sync/net/secureservice/handshake/handshakeproto" + "github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey" + "github.com/libp2p/go-libp2p/core/sec" + "go.uber.org/zap" +) + +func newNoVerifyChecker() handshake.CredentialChecker { + return &noVerifyChecker{cred: &handshakeproto.Credentials{Type: handshakeproto.CredentialsType_SkipVerify}} +} + +type noVerifyChecker struct { + cred *handshakeproto.Credentials +} + +func (n noVerifyChecker) MakeCredentials(sc sec.SecureConn) *handshakeproto.Credentials { + return n.cred +} + +func (n noVerifyChecker) CheckCredential(sc sec.SecureConn, cred *handshakeproto.Credentials) (identity []byte, err error) { + return nil, nil +} + +func newPeerSignVerifier(account *accountdata.AccountData) handshake.CredentialChecker { + return &peerSignVerifier{account: account} +} + +type peerSignVerifier struct { + account *accountdata.AccountData +} + +func (p *peerSignVerifier) MakeCredentials(sc sec.SecureConn) *handshakeproto.Credentials { + sign, err := p.account.SignKey.Sign([]byte(p.account.PeerId + sc.RemotePeer().String())) + if err != nil { + log.Warn("can't sign identity credentials", zap.Error(err)) + } + msg := &handshakeproto.PayloadSignedPeerIds{ + Identity: p.account.Identity, + Sign: sign, + } + payload, _ := msg.Marshal() + return &handshakeproto.Credentials{ + Type: handshakeproto.CredentialsType_SignedPeerIds, + Payload: payload, + } +} + +func (p *peerSignVerifier) CheckCredential(sc sec.SecureConn, cred *handshakeproto.Credentials) (identity []byte, err error) { + if cred.Type != handshakeproto.CredentialsType_SignedPeerIds { + return nil, handshake.ErrSkipVerifyNotAllowed + } + var msg = &handshakeproto.PayloadSignedPeerIds{} + if err = msg.Unmarshal(cred.Payload); err != nil { + return nil, handshake.ErrUnexpectedPayload + } + pubKey, err := signingkey.NewSigningEd25519PubKeyFromBytes(msg.Identity) + if err != nil { + return nil, handshake.ErrInvalidCredentials + } + ok, err := pubKey.Verify([]byte((sc.RemotePeer().String() + p.account.PeerId)), msg.Sign) + if err != nil { + return nil, err + } + if !ok { + return nil, handshake.ErrInvalidCredentials + } + return msg.Identity, nil +} diff --git a/net/secureservice/credential_test.go b/net/secureservice/credential_test.go new file mode 100644 index 00000000..9b005f29 --- /dev/null +++ b/net/secureservice/credential_test.go @@ -0,0 +1,77 @@ +package secureservice + +import ( + "github.com/anytypeio/any-sync/commonspace/object/accountdata" + "github.com/anytypeio/any-sync/net/secureservice/handshake" + "github.com/anytypeio/any-sync/testutil/accounttest" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/sec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net" + "testing" +) + +func TestPeerSignVerifier_CheckCredential(t *testing.T) { + a1 := newTestAccData(t) + a2 := newTestAccData(t) + + cc1 := newPeerSignVerifier(a1) + cc2 := newPeerSignVerifier(a2) + + c1 := newTestSC(a2.PeerId) + c2 := newTestSC(a1.PeerId) + + cr1 := cc1.MakeCredentials(c1) + cr2 := cc2.MakeCredentials(c2) + id1, err := cc1.CheckCredential(c1, cr2) + assert.NoError(t, err) + assert.Equal(t, a2.Identity, id1) + + id2, err := cc2.CheckCredential(c2, cr1) + assert.NoError(t, err) + assert.Equal(t, a1.Identity, id2) + + _, err = cc1.CheckCredential(c1, cr1) + assert.EqualError(t, err, handshake.ErrInvalidCredentials.Error()) +} + +func newTestAccData(t *testing.T) *accountdata.AccountData { + as := accounttest.AccountTestService{} + require.NoError(t, as.Init(nil)) + return as.Account() +} + +func newTestSC(peerId string) sec.SecureConn { + pid, _ := peer.Decode(peerId) + return &testSc{ + ID: pid, + } +} + +type testSc struct { + net.Conn + peer.ID +} + +func (t *testSc) LocalPeer() peer.ID { + return "" +} + +func (t *testSc) LocalPrivateKey() crypto.PrivKey { + return nil +} + +func (t *testSc) RemotePeer() peer.ID { + return t.ID +} + +func (t *testSc) RemotePublicKey() crypto.PubKey { + return nil +} + +func (t *testSc) ConnState() network.ConnectionState { + return network.ConnectionState{} +} diff --git a/net/secureservice/secureservice.go b/net/secureservice/secureservice.go index f55ad8da..a6cb7544 100644 --- a/net/secureservice/secureservice.go +++ b/net/secureservice/secureservice.go @@ -5,6 +5,9 @@ import ( commonaccount "github.com/anytypeio/any-sync/accountservice" "github.com/anytypeio/any-sync/app" "github.com/anytypeio/any-sync/app/logger" + "github.com/anytypeio/any-sync/commonspace/object/accountdata" + "github.com/anytypeio/any-sync/net/secureservice/handshake" + "github.com/anytypeio/any-sync/nodeconf" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/sec" libp2ptls "github.com/libp2p/go-libp2p/p2p/security/tls" @@ -41,7 +44,13 @@ type SecureService interface { } type secureService struct { - key crypto.PrivKey + outboundTr *libp2ptls.Transport + account *accountdata.AccountData + key crypto.PrivKey + nodeconf nodeconf.Service + + noVerifyChecker handshake.CredentialChecker + peerSignVerifier handshake.CredentialChecker } func (s *secureService) Init(a *app.App) (err error) { @@ -54,8 +63,12 @@ func (s *secureService) Init(a *app.App) (err error) { return } - log.Info("secure service init", zap.String("peerId", account.Account().PeerId)) + s.noVerifyChecker = newNoVerifyChecker() + s.peerSignVerifier = newPeerSignVerifier(account.Account()) + s.nodeconf = a.MustComponent(nodeconf.CName).(nodeconf.Service) + + log.Info("secure service init", zap.String("peerId", account.Account().PeerId)) return nil } @@ -72,9 +85,22 @@ func (s *secureService) BasicListener(lis net.Listener, timeoutMillis int) Conte } func (s *secureService) TLSConn(ctx context.Context, conn net.Conn) (sec.SecureConn, error) { - tr, err := libp2ptls.New(libp2ptls.ID, s.key, nil) + sc, err := s.outboundTr.SecureOutbound(ctx, conn, "") if err != nil { - return nil, err + return nil, HandshakeError{err: err, remoteAddr: conn.RemoteAddr().String()} } - return tr.SecureOutbound(ctx, conn, "") + peerId := sc.RemotePeer().String() + confTypes := s.nodeconf.GetLast().NodeTypes(peerId) + var checker handshake.CredentialChecker + if len(confTypes) > 0 { + checker = s.peerSignVerifier + } else { + checker = s.noVerifyChecker + } + // ignore identity for outgoing connection because we don't need it at this moment + _, err = handshake.OutgoingHandshake(sc, checker) + if err != nil { + return nil, HandshakeError{err: err, remoteAddr: conn.RemoteAddr().String()} + } + return sc, nil } diff --git a/net/secureservice/secureservice_test.go b/net/secureservice/secureservice_test.go new file mode 100644 index 00000000..686b3cac --- /dev/null +++ b/net/secureservice/secureservice_test.go @@ -0,0 +1,35 @@ +package secureservice + +import ( + "context" + "github.com/anytypeio/any-sync/app" + "github.com/anytypeio/any-sync/testutil/accounttest" + "github.com/stretchr/testify/require" + "testing" +) + +var ctx = context.Background() + +func TestHandshake(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) +} + +func newFixture(t *testing.T) *fixture { + fx := &fixture{ + secureService: New().(*secureService), + a: new(app.App), + } + fx.a.Register(&accounttest.AccountTestService{}).Register(fx.secureService) + require.NoError(t, fx.a.Start(ctx)) + return fx +} + +type fixture struct { + *secureService + a *app.App +} + +func (fx *fixture) Finish(t *testing.T) { + require.NoError(t, fx.a.Close(ctx)) +} diff --git a/net/secureservice/tlslistener.go b/net/secureservice/tlslistener.go index 867ced26..6abf5742 100644 --- a/net/secureservice/tlslistener.go +++ b/net/secureservice/tlslistener.go @@ -3,6 +3,7 @@ package secureservice import ( "context" "github.com/anytypeio/any-sync/net/peer" + "github.com/anytypeio/any-sync/net/secureservice/handshake" "github.com/anytypeio/any-sync/net/timeoutconn" "github.com/libp2p/go-libp2p/core/crypto" libp2ptls "github.com/libp2p/go-libp2p/p2p/security/tls" @@ -22,9 +23,10 @@ type ContextListener interface { Addr() net.Addr } -func newTLSListener(key crypto.PrivKey, lis net.Listener, timeoutMillis int) ContextListener { +func newTLSListener(cc handshake.CredentialChecker, key crypto.PrivKey, lis net.Listener, timeoutMillis int) ContextListener { tr, _ := libp2ptls.New(libp2ptls.ID, key, nil) return &tlsListener{ + cc: cc, tr: tr, Listener: lis, timeoutMillis: timeoutMillis, @@ -35,6 +37,7 @@ type tlsListener struct { net.Listener tr *libp2ptls.Transport timeoutMillis int + cc handshake.CredentialChecker } func (p *tlsListener) Accept(ctx context.Context) (context.Context, net.Conn, error) { @@ -54,6 +57,14 @@ func (p *tlsListener) upgradeConn(ctx context.Context, conn net.Conn) (context.C err: err, } } + identity, err := handshake.IncomingHandshake(secure, p.cc) + if err != nil { + return nil, nil, HandshakeError{ + remoteAddr: conn.RemoteAddr().String(), + err: err, + } + } ctx = peer.CtxWithPeerId(ctx, secure.RemotePeer().String()) + ctx = peer.CtxWithIdentity(ctx, identity) return ctx, secure, nil } diff --git a/nodeconf/configuration.go b/nodeconf/configuration.go index c3b511b9..bc913fd6 100644 --- a/nodeconf/configuration.go +++ b/nodeconf/configuration.go @@ -23,6 +23,8 @@ type Configuration interface { CHash() chash.CHash // Partition returns partition number by spaceId Partition(spaceId string) (part int) + // NodeTypes returns list of known nodeTypes by nodeId, if node not registered in configuration will return empty list + NodeTypes(nodeId string) []NodeType } type configuration struct { @@ -82,6 +84,15 @@ func (c *configuration) Partition(spaceId string) (part int) { return c.chash.GetPartition(ReplKey(spaceId)) } +func (c *configuration) NodeTypes(nodeId string) []NodeType { + for _, m := range c.allMembers { + if m.PeerId == nodeId { + return m.Types + } + } + return nil +} + func ReplKey(spaceId string) (replKey string) { if i := strings.LastIndex(spaceId, "."); i != -1 { return spaceId[i+1:] diff --git a/testutil/accounttest/accountservice.go b/testutil/accounttest/accountservice.go index e745f1e7..6b5cedac 100644 --- a/testutil/accounttest/accountservice.go +++ b/testutil/accounttest/accountservice.go @@ -34,15 +34,21 @@ func (s *AccountTestService) Init(a *app.App) (err error) { return } - peerId, err := peer.IdFromSigningPubKey(signKey.GetPublic()) + peerKey, _, err := signingkey.GenerateRandomEd25519KeyPair() + if err != nil { + return err + } + + peerId, err := peer.IdFromSigningPubKey(peerKey.GetPublic()) if err != nil { return err } s.acc = &accountdata.AccountData{ - PeerId: peerId.String(), Identity: ident, + PeerKey: peerKey, SignKey: signKey, EncKey: encKey, + PeerId: peerId.String(), } return nil }