Skip to content

Commit cde2640

Browse files
committed
[FAB-2007] Gossip: External and internal endpoints II
Background: ------------ The previous commit added support in the discovery layer for policies that effect the membership response handling logic. That logic is based on the selfInformation field which is a signed GossipMessage that contains an AliveMessage and is supposed to represent the remote peer that sent the membership request. Even though the message is validated, nothing prevents an attacker to replay a signedMessage he recorded. There is a TODO in the code that says: https://github.com/hyperledger/fabric/blob/master/gossip/discovery/discovery_impl.go#L293 // TODO: make sure somehow that the membership request is "fresh" This is to prevent replay attacks, and: 1) This needs to be addressed for FAB-2007 2) A replay attack can happen anyway if a malicious peer gets hold of such a message early enough befor the attacked peer got the message What's in this commit? ---------------------- This commit leverages the fact that a membership request is point-to-point, and checks that the sender of the membership request is the same peer that is on the other side of the connection. Also removed the TODO since now it's not needed anymore. How is this tested? -------------------- I added a test that creates such a replay attack and spoofs a membership request, and compares it to a valid membership request to demonstrate that the attack prevention works. Change-Id: I8c994b5627189a1d0fb3f6a7d9edbd9a9c021b2c Signed-off-by: Yacov Manevich <[email protected]>
1 parent 4579ed1 commit cde2640

File tree

3 files changed

+87
-9
lines changed

3 files changed

+87
-9
lines changed

gossip/discovery/discovery_impl.go

-2
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,6 @@ func (d *gossipDiscoveryImpl) handleMsgFromComm(m *proto.SignedGossipMessage) {
292292
d.logger.Debug("Got message:", m)
293293
defer d.logger.Debug("Exiting")
294294

295-
// TODO: make sure somehow that the membership request is "fresh"
296295
if memReq := m.GetMemReq(); memReq != nil {
297296
selfInfoGossipMsg, err := memReq.SelfInformation.ToGossipMessage()
298297
if err != nil {
@@ -395,7 +394,6 @@ func (d *gossipDiscoveryImpl) createMembershipResponse(targetMember *NetworkMemb
395394
defer d.lock.RUnlock()
396395

397396
deadPeers := []*proto.Envelope{}
398-
399397
for _, dm := range d.deadMembership.ToSlice() {
400398

401399
if !shouldBeDisclosed(dm) {

gossip/gossip/gossip_impl.go

+17-7
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,23 @@ func (g *gossipServiceImpl) handleMessage(m proto.ReceivedMessage) {
329329
}
330330

331331
if selectOnlyDiscoveryMessages(m) {
332+
// It's a membership request, check its self information
333+
// matches the sender
334+
if m.GetGossipMessage().GetMemReq() != nil {
335+
sMsg, err := m.GetGossipMessage().GetMemReq().SelfInformation.ToGossipMessage()
336+
if err != nil {
337+
g.logger.Warning("Got membership request with invalid selfInfo:", err)
338+
return
339+
}
340+
if !sMsg.IsAliveMsg() {
341+
g.logger.Warning("Got membership request with selfInfo that isn't an AliveMessage")
342+
return
343+
}
344+
if !bytes.Equal(sMsg.GetAliveMsg().Membership.PkiID, m.GetConnectionInfo().ID) {
345+
g.logger.Warning("Got membership request with selfInfo that doesn't match the handshake")
346+
return
347+
}
348+
}
332349
g.forwardDiscoveryMsg(m)
333350
}
334351

@@ -385,13 +402,6 @@ func (g *gossipServiceImpl) validateMsg(msg proto.ReceivedMessage) bool {
385402
return true
386403
}
387404

388-
func (g *gossipServiceImpl) forwardToDiscoveryLayer(msg proto.ReceivedMessage) {
389-
defer func() { // can be closed while shutting down
390-
recover()
391-
}()
392-
g.discAdapter.incChan <- msg.GetGossipMessage()
393-
}
394-
395405
func (g *gossipServiceImpl) sendGossipBatch(a []interface{}) {
396406
msgs2Gossip := make([]*proto.SignedGossipMessage, len(a))
397407
for i, e := range a {

gossip/gossip/gossip_test.go

+70
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"time"
3030

3131
"github.com/hyperledger/fabric/gossip/api"
32+
"github.com/hyperledger/fabric/gossip/comm"
3233
"github.com/hyperledger/fabric/gossip/common"
3334
"github.com/hyperledger/fabric/gossip/discovery"
3435
"github.com/hyperledger/fabric/gossip/gossip/algo"
@@ -665,6 +666,75 @@ func TestMembershipConvergence(t *testing.T) {
665666
testWG.Done()
666667
}
667668

669+
func TestMembershipRequestSpoofing(t *testing.T) {
670+
t.Parallel()
671+
// Scenario: g1, g2, g3 are peers, and g2 is malicious, and wants
672+
// to impersonate g3 when sending a membership request to g1.
673+
// Expected output: g1 should *NOT* respond to g2,
674+
// However, g1 should respond to g3 when it sends the message itself.
675+
676+
portPrefix := 2000
677+
g1 := newGossipInstance(portPrefix, 0, 100)
678+
g2 := newGossipInstance(portPrefix, 1, 100, 2)
679+
g3 := newGossipInstance(portPrefix, 2, 100, 1)
680+
defer g1.Stop()
681+
defer g2.Stop()
682+
defer g3.Stop()
683+
684+
// Wait for g2 and g3 to know about each other
685+
waitUntilOrFail(t, checkPeersMembership(t, []Gossip{g2, g3}, 1))
686+
// Obtain an alive message from p3
687+
_, aliveMsgChan := g2.Accept(func(o interface{}) bool {
688+
msg := o.(proto.ReceivedMessage).GetGossipMessage()
689+
// Make sure we get an AliveMessage and it's about g3
690+
return msg.IsAliveMsg() && bytes.Equal(msg.GetAliveMsg().Membership.PkiID, []byte("localhost:2002"))
691+
}, true)
692+
aliveMsg := <-aliveMsgChan
693+
694+
// Obtain channel for messages from g1 to g2
695+
_, g1ToG2 := g2.Accept(func(o interface{}) bool {
696+
connInfo := o.(proto.ReceivedMessage).GetConnectionInfo()
697+
return bytes.Equal([]byte("localhost:2000"), connInfo.ID)
698+
}, true)
699+
700+
// Obtain channel for messages from g1 to g3
701+
_, g1ToG3 := g3.Accept(func(o interface{}) bool {
702+
connInfo := o.(proto.ReceivedMessage).GetConnectionInfo()
703+
return bytes.Equal([]byte("localhost:2000"), connInfo.ID)
704+
}, true)
705+
706+
// Now, create a membership request message
707+
memRequestSpoofFactory := func(aliveMsgEnv *proto.Envelope) *proto.SignedGossipMessage {
708+
return (&proto.GossipMessage{
709+
Tag: proto.GossipMessage_EMPTY,
710+
Nonce: uint64(0),
711+
Content: &proto.GossipMessage_MemReq{
712+
MemReq: &proto.MembershipRequest{
713+
SelfInformation: aliveMsgEnv,
714+
Known: [][]byte{},
715+
},
716+
},
717+
}).NoopSign()
718+
}
719+
spoofedMemReq := memRequestSpoofFactory(aliveMsg.GetSourceEnvelope())
720+
g2.Send(spoofedMemReq.GossipMessage, &comm.RemotePeer{Endpoint: "localhost:2000", PKIID: common.PKIidType("localhost:2000")})
721+
select {
722+
case <-time.After(time.Second):
723+
break
724+
case <-g1ToG2:
725+
assert.Fail(t, "Received response from g1 but shouldn't have")
726+
}
727+
728+
// Now send the same message from g3 to g1
729+
g3.Send(spoofedMemReq.GossipMessage, &comm.RemotePeer{Endpoint: "localhost:2000", PKIID: common.PKIidType("localhost:2000")})
730+
select {
731+
case <-time.After(time.Second):
732+
assert.Fail(t, "Didn't receive a message back from g1 on time")
733+
case <-g1ToG3:
734+
break
735+
}
736+
}
737+
668738
func TestDataLeakage(t *testing.T) {
669739
t.Parallel()
670740
portPrefix := 1610

0 commit comments

Comments
 (0)