Skip to content

Commit 52bb909

Browse files
authored
Properly handle expired identities in gossip (release-2.5) (#5122)
* Properly handle expired identities in gossip When a peer's certificate expires, gossip still retains past messages it has sent, and gossips them to other peers. Aside from peers doing redundant work, this also impairs their connectivity to the peer with the renewed certificate. The reason is that peers try connect to the peer of the renewed certificate but abort because they cannot find its (old) PKI-ID in the identity store, which purged its old PKI-ID once its certificate has expired. This commit fixes this problem by making the peer forget about peers that their identities have been purged from the identity store. Signed-off-by: David Enyeart <enyeart@us.ibm.com> Signed-off-by: Yacov Manevich <yacov.manevich@gmail.com> * Fix gossip cert renewal integration test The membership check via discovery does not work consistently due to the renewed cert signature not matching expectations. For now, it is sufficient to do the membership check via checking the log. Signed-off-by: David Enyeart <enyeart@us.ibm.com> --------- Signed-off-by: David Enyeart <enyeart@us.ibm.com> Signed-off-by: Yacov Manevich <yacov.manevich@gmail.com>
1 parent a55f8b8 commit 52bb909

File tree

5 files changed

+183
-4
lines changed

5 files changed

+183
-4
lines changed

gossip/comm/comm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ type Comm interface {
4545
PresumedDead() <-chan common.PKIidType
4646

4747
// IdentitySwitch returns a read-only channel about identity change events
48-
IdentitySwitch() <-chan common.PKIidType
48+
IdentitySwitch() chan common.PKIidType
4949

5050
// CloseConn closes a connection to a certain endpoint
5151
CloseConn(peer *RemotePeer)

gossip/comm/comm_impl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func (c *commImpl) PresumedDead() <-chan common.PKIidType {
359359
return c.deadEndpoints
360360
}
361361

362-
func (c *commImpl) IdentitySwitch() <-chan common.PKIidType {
362+
func (c *commImpl) IdentitySwitch() chan common.PKIidType {
363363
return c.identityChanges
364364
}
365365

gossip/comm/mock/mock_comm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func (mock *commMock) start() {
134134
}
135135
}
136136

137-
func (mock *commMock) IdentitySwitch() <-chan common.PKIidType {
137+
func (mock *commMock) IdentitySwitch() chan common.PKIidType {
138138
panic("implement me")
139139
}
140140

gossip/gossip/gossip_impl.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,24 @@ func New(conf *Config, s *grpc.Server, sa api.SecurityAdvisor,
9696
g.stateInfoMsgStore = g.newStateInfoMsgStore()
9797

9898
g.idMapper = identity.NewIdentityMapper(mcs, selfIdentity, func(pkiID common.PKIidType, identity api.PeerIdentityType) {
99-
g.comm.CloseConn(&comm.RemotePeer{PKIID: pkiID})
99+
// Identities which are purged from the membership store
100+
// should not be communicated with anymore, as the purge is done
101+
// because the identity of the corresponding PKI-ID not being
102+
// valid anymore, such as it being expired.
103+
104+
// Remove the identity from the identity replication
105+
// for it to not be replicated any further.
100106
g.certPuller.Remove(string(pkiID))
107+
108+
// Notify the identity switch channel of the communication layer,
109+
// which in turn is used to notify the discovery layer
110+
// about the PKI-ID not being relevant anymore, which causes
111+
// the discovery layer to purge it from memory.
112+
// Afterwards, gossip never communicates with this PKI-ID.
113+
g.comm.IdentitySwitch() <- pkiID
114+
115+
// Cease communication with the node, if connected to it.
116+
g.comm.CloseConn(&comm.RemotePeer{PKIID: pkiID})
101117
}, sa)
102118

103119
commConfig := comm.CommConfig{

integration/gossip/gossip_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@
77
package gossip
88

99
import (
10+
"crypto/ecdsa"
11+
"crypto/rand"
12+
"crypto/sha256"
13+
"crypto/x509"
14+
"encoding/pem"
1015
"fmt"
1116
"io/ioutil"
17+
"math/big"
1218
"os"
19+
"path/filepath"
1320
"syscall"
1421
"time"
1522

@@ -265,8 +272,164 @@ var _ = Describe("Gossip State Transfer and Membership", func() {
265272
assertPeerMembershipUpdate(network, peer1Org1, []*nwo.Peer{peer0Org2, peer1Org2}, nwprocs, expectedMsgFromExpirationCallback)
266273
})
267274
})
275+
276+
It("updates membership for a peer with a renewed certificate", func() {
277+
network.Bootstrap()
278+
orderer := network.Orderer("orderer")
279+
nwprocs.ordererRunner = network.OrdererRunner(orderer)
280+
nwprocs.ordererProcess = ifrit.Invoke(nwprocs.ordererRunner)
281+
Eventually(nwprocs.ordererProcess.Ready(), network.EventuallyTimeout).Should(BeClosed())
282+
283+
peer0Org1 := network.Peer("Org1", "peer0")
284+
peer0Org2 := network.Peer("Org2", "peer0")
285+
286+
By("bringing up a peer in each organization")
287+
startPeers(nwprocs, false, peer0Org1, peer0Org2)
288+
289+
By("creating and joining both peers to channel")
290+
network.CreateChannel(channelName, orderer, peer0Org1)
291+
network.JoinChannel(channelName, orderer, peer0Org1, peer0Org2)
292+
network.UpdateChannelAnchors(orderer, channelName)
293+
294+
By("verifying membership of both peers")
295+
Eventually(nwo.DiscoverPeers(network, peer0Org1, "User1", "testchannel"), 50*time.Second, 100*time.Millisecond).Should(ContainElements(network.DiscoveredPeer(peer0Org2, "_lifecycle")))
296+
297+
By("stopping, renewing peer0Org2 certificate before expiration, and restarting")
298+
stopPeers(nwprocs, peer0Org2)
299+
renewPeerCertificate(network, peer0Org2, time.Now().Add(time.Minute))
300+
startPeers(nwprocs, false, peer0Org2)
301+
302+
By("verifying membership after cert renewed")
303+
peer0Org1Runner := nwprocs.peerRunners[peer0Org1.ID()]
304+
Eventually(peer0Org1Runner.Err(), network.EventuallyTimeout).Should(gbytes.Say("Membership view has changed. peers went online"))
305+
/*
306+
// TODO - Replace membership log check with membership discovery check (not currently working since renewed cert signature doesn't always match expectations even though it is forced to be Low-S)
307+
Eventually(
308+
nwo.DiscoverPeers(network, peer0Org1, "User1", "testchannel"),
309+
60*time.Second,
310+
100*time.Millisecond).
311+
Should(ContainElements(network.DiscoveredPeer(network.Peer("Org2", "peer0"), "_lifecycle")))
312+
*/
313+
314+
By("waiting for cert to expire within a minute")
315+
Eventually(peer0Org1Runner.Err(), time.Minute*2).Should(gbytes.Say("gossipping peer identity expired"))
316+
317+
By("stopping, renewing peer0Org2 certificate again after its expiration, restarting")
318+
stopPeers(nwprocs, peer0Org2)
319+
renewPeerCertificate(network, peer0Org2, time.Now().Add(time.Hour))
320+
startPeers(nwprocs, false, peer0Org2)
321+
322+
By("verifying membership after cert expired and renewed again")
323+
Eventually(peer0Org1Runner.Err(), network.EventuallyTimeout).Should(gbytes.Say("Membership view has changed. peers went online"))
324+
325+
/*
326+
// TODO - Replace membership log check with membership discovery check (not currently working since renewed cert signature doesn't always match expectations even though it is forced to be Low-S)
327+
Eventually(
328+
nwo.DiscoverPeers(network, peer0Org1, "User1", "testchannel"),
329+
60*time.Second,
330+
100*time.Millisecond).
331+
Should(ContainElements(network.DiscoveredPeer(network.Peer("Org2", "peer0"), "_lifecycle")))
332+
*/
333+
})
268334
})
269335

336+
// renewPeerCertificate renews the certificate with a given expirationTime and re-writes it to the peer's signcert directory
337+
func renewPeerCertificate(network *nwo.Network, peer *nwo.Peer, expirationTime time.Time) {
338+
peerDomain := network.Organization(peer.Organization).Domain
339+
340+
peerCAKeyPath := filepath.Join(network.RootDir, "crypto", "peerOrganizations", peerDomain, "ca", "priv_sk")
341+
peerCAKey, err := os.ReadFile(peerCAKeyPath)
342+
Expect(err).NotTo(HaveOccurred())
343+
344+
peerCACertPath := filepath.Join(network.RootDir, "crypto", "peerOrganizations", peerDomain, "ca", fmt.Sprintf("ca.%s-cert.pem", peerDomain))
345+
peerCACert, err := os.ReadFile(peerCACertPath)
346+
Expect(err).NotTo(HaveOccurred())
347+
348+
peerCertPath := filepath.Join(network.PeerLocalMSPDir(peer), "signcerts", fmt.Sprintf("peer0.%s-cert.pem", peerDomain))
349+
peerCert, err := os.ReadFile(peerCertPath)
350+
Expect(err).NotTo(HaveOccurred())
351+
352+
renewedCert, _ := expireCertificate(peerCert, peerCACert, peerCAKey, expirationTime)
353+
err = os.WriteFile(peerCertPath, renewedCert, 0o600)
354+
Expect(err).NotTo(HaveOccurred())
355+
}
356+
357+
// expireCertificate re-creates and re-signs a certificate with a new expirationTime
358+
func expireCertificate(certPEM, caCertPEM, caKeyPEM []byte, expirationTime time.Time) (expiredcertPEM []byte, earlyMadeCACertPEM []byte) {
359+
keyAsDER, _ := pem.Decode(caKeyPEM)
360+
caKeyWithoutType, err := x509.ParsePKCS8PrivateKey(keyAsDER.Bytes)
361+
Expect(err).NotTo(HaveOccurred())
362+
caKey := caKeyWithoutType.(*ecdsa.PrivateKey)
363+
364+
caCertAsDER, _ := pem.Decode(caCertPEM)
365+
caCert, err := x509.ParseCertificate(caCertAsDER.Bytes)
366+
Expect(err).NotTo(HaveOccurred())
367+
368+
certAsDER, _ := pem.Decode(certPEM)
369+
cert, err := x509.ParseCertificate(certAsDER.Bytes)
370+
Expect(err).NotTo(HaveOccurred())
371+
372+
cert.Raw = nil
373+
caCert.Raw = nil
374+
// The certificate was made 1 minute ago (1 hour doesn't work since cert will be before original CA cert NotBefore time)
375+
cert.NotBefore = time.Now().Add((-1) * time.Minute)
376+
// As well as the CA certificate
377+
caCert.NotBefore = time.Now().Add((-1) * time.Minute)
378+
// The certificate expires now
379+
cert.NotAfter = expirationTime
380+
381+
// The CA creates and signs a temporary certificate
382+
tempCertBytes, err := x509.CreateCertificate(rand.Reader, cert, caCert, cert.PublicKey, caKey)
383+
Expect(err).NotTo(HaveOccurred())
384+
385+
// Force the certificate to use Low-S signature to be compatible with the identities that Fabric uses
386+
387+
// Parse the certificate to extract the TBS (to-be-signed) data
388+
tempParsedCert, err := x509.ParseCertificate(tempCertBytes)
389+
Expect(err).NotTo(HaveOccurred())
390+
391+
// Hash the TBS data
392+
hash := sha256.Sum256(tempParsedCert.RawTBSCertificate)
393+
394+
// Sign the hash using forceLowS
395+
r, s, err := forceLowS(caKey, hash[:])
396+
Expect(err).NotTo(HaveOccurred())
397+
398+
// Encode the signature (DER format)
399+
signature := append(r.Bytes(), s.Bytes()...)
400+
401+
// Replace the signature in the certificate with the low-s signature
402+
tempParsedCert.Signature = signature
403+
404+
// Re-encode the certificate with the low-s signature
405+
certBytes, err := x509.CreateCertificate(rand.Reader, tempParsedCert, caCert, cert.PublicKey, caKey)
406+
Expect(err).NotTo(HaveOccurred())
407+
408+
// The CA signs its own certificate
409+
caCertBytes, err := x509.CreateCertificate(rand.Reader, caCert, caCert, caCert.PublicKey, caKey)
410+
Expect(err).NotTo(HaveOccurred())
411+
412+
expiredcertPEM = pem.EncodeToMemory(&pem.Block{Bytes: certBytes, Type: "CERTIFICATE"})
413+
earlyMadeCACertPEM = pem.EncodeToMemory(&pem.Block{Bytes: caCertBytes, Type: "CERTIFICATE"})
414+
return
415+
}
416+
417+
// forceLowS ensures the ECDSA signature's S value is low
418+
func forceLowS(priv *ecdsa.PrivateKey, hash []byte) (r, s *big.Int, err error) {
419+
r, s, err = ecdsa.Sign(rand.Reader, priv, hash)
420+
Expect(err).NotTo(HaveOccurred())
421+
422+
curveOrder := priv.Curve.Params().N
423+
halfOrder := new(big.Int).Rsh(curveOrder, 1) // curveOrder / 2
424+
425+
// If s is greater than half the order, replace it with curveOrder - s
426+
if s.Cmp(halfOrder) > 0 {
427+
s.Sub(curveOrder, s)
428+
}
429+
430+
return r, s, nil
431+
}
432+
270433
func runTransactions(n *nwo.Network, orderer *nwo.Orderer, peer *nwo.Peer, chaincodeName string, channelID string) {
271434
for i := 0; i < 5; i++ {
272435
sess, err := n.PeerUserSession(peer, "User1", commands.ChaincodeInvoke{

0 commit comments

Comments
 (0)