Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly handle expired identities in gossip (release-2.5) #5122

Merged
merged 2 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gossip/comm/comm.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type Comm interface {
PresumedDead() <-chan common.PKIidType

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

// CloseConn closes a connection to a certain endpoint
CloseConn(peer *RemotePeer)
Expand Down
2 changes: 1 addition & 1 deletion gossip/comm/comm_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ func (c *commImpl) PresumedDead() <-chan common.PKIidType {
return c.deadEndpoints
}

func (c *commImpl) IdentitySwitch() <-chan common.PKIidType {
func (c *commImpl) IdentitySwitch() chan common.PKIidType {
return c.identityChanges
}

Expand Down
2 changes: 1 addition & 1 deletion gossip/comm/mock/mock_comm.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func (mock *commMock) start() {
}
}

func (mock *commMock) IdentitySwitch() <-chan common.PKIidType {
func (mock *commMock) IdentitySwitch() chan common.PKIidType {
panic("implement me")
}

Expand Down
18 changes: 17 additions & 1 deletion gossip/gossip/gossip_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,24 @@ func New(conf *Config, s *grpc.Server, sa api.SecurityAdvisor,
g.stateInfoMsgStore = g.newStateInfoMsgStore()

g.idMapper = identity.NewIdentityMapper(mcs, selfIdentity, func(pkiID common.PKIidType, identity api.PeerIdentityType) {
g.comm.CloseConn(&comm.RemotePeer{PKIID: pkiID})
// Identities which are purged from the membership store
// should not be communicated with anymore, as the purge is done
// because the identity of the corresponding PKI-ID not being
// valid anymore, such as it being expired.

// Remove the identity from the identity replication
// for it to not be replicated any further.
g.certPuller.Remove(string(pkiID))

// Notify the identity switch channel of the communication layer,
// which in turn is used to notify the discovery layer
// about the PKI-ID not being relevant anymore, which causes
// the discovery layer to purge it from memory.
// Afterwards, gossip never communicates with this PKI-ID.
g.comm.IdentitySwitch() <- pkiID

// Cease communication with the node, if connected to it.
g.comm.CloseConn(&comm.RemotePeer{PKIID: pkiID})
}, sa)

commConfig := comm.CommConfig{
Expand Down
163 changes: 163 additions & 0 deletions integration/gossip/gossip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@
package gossip

import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"syscall"
"time"

Expand Down Expand Up @@ -265,8 +272,164 @@ var _ = Describe("Gossip State Transfer and Membership", func() {
assertPeerMembershipUpdate(network, peer1Org1, []*nwo.Peer{peer0Org2, peer1Org2}, nwprocs, expectedMsgFromExpirationCallback)
})
})

It("updates membership for a peer with a renewed certificate", func() {
network.Bootstrap()
orderer := network.Orderer("orderer")
nwprocs.ordererRunner = network.OrdererRunner(orderer)
nwprocs.ordererProcess = ifrit.Invoke(nwprocs.ordererRunner)
Eventually(nwprocs.ordererProcess.Ready(), network.EventuallyTimeout).Should(BeClosed())

peer0Org1 := network.Peer("Org1", "peer0")
peer0Org2 := network.Peer("Org2", "peer0")

By("bringing up a peer in each organization")
startPeers(nwprocs, false, peer0Org1, peer0Org2)

By("creating and joining both peers to channel")
network.CreateChannel(channelName, orderer, peer0Org1)
network.JoinChannel(channelName, orderer, peer0Org1, peer0Org2)
network.UpdateChannelAnchors(orderer, channelName)

By("verifying membership of both peers")
Eventually(nwo.DiscoverPeers(network, peer0Org1, "User1", "testchannel"), 50*time.Second, 100*time.Millisecond).Should(ContainElements(network.DiscoveredPeer(peer0Org2, "_lifecycle")))

By("stopping, renewing peer0Org2 certificate before expiration, and restarting")
stopPeers(nwprocs, peer0Org2)
renewPeerCertificate(network, peer0Org2, time.Now().Add(time.Minute))
startPeers(nwprocs, false, peer0Org2)

By("verifying membership after cert renewed")
peer0Org1Runner := nwprocs.peerRunners[peer0Org1.ID()]
Eventually(peer0Org1Runner.Err(), network.EventuallyTimeout).Should(gbytes.Say("Membership view has changed. peers went online"))
/*
// 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)
Eventually(
nwo.DiscoverPeers(network, peer0Org1, "User1", "testchannel"),
60*time.Second,
100*time.Millisecond).
Should(ContainElements(network.DiscoveredPeer(network.Peer("Org2", "peer0"), "_lifecycle")))
*/

By("waiting for cert to expire within a minute")
Eventually(peer0Org1Runner.Err(), time.Minute*2).Should(gbytes.Say("gossipping peer identity expired"))

By("stopping, renewing peer0Org2 certificate again after its expiration, restarting")
stopPeers(nwprocs, peer0Org2)
renewPeerCertificate(network, peer0Org2, time.Now().Add(time.Hour))
startPeers(nwprocs, false, peer0Org2)

By("verifying membership after cert expired and renewed again")
Eventually(peer0Org1Runner.Err(), network.EventuallyTimeout).Should(gbytes.Say("Membership view has changed. peers went online"))

/*
// 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)
Eventually(
nwo.DiscoverPeers(network, peer0Org1, "User1", "testchannel"),
60*time.Second,
100*time.Millisecond).
Should(ContainElements(network.DiscoveredPeer(network.Peer("Org2", "peer0"), "_lifecycle")))
*/
})
})

// renewPeerCertificate renews the certificate with a given expirationTime and re-writes it to the peer's signcert directory
func renewPeerCertificate(network *nwo.Network, peer *nwo.Peer, expirationTime time.Time) {
peerDomain := network.Organization(peer.Organization).Domain

peerCAKeyPath := filepath.Join(network.RootDir, "crypto", "peerOrganizations", peerDomain, "ca", "priv_sk")
peerCAKey, err := os.ReadFile(peerCAKeyPath)
Expect(err).NotTo(HaveOccurred())

peerCACertPath := filepath.Join(network.RootDir, "crypto", "peerOrganizations", peerDomain, "ca", fmt.Sprintf("ca.%s-cert.pem", peerDomain))
peerCACert, err := os.ReadFile(peerCACertPath)
Expect(err).NotTo(HaveOccurred())

peerCertPath := filepath.Join(network.PeerLocalMSPDir(peer), "signcerts", fmt.Sprintf("peer0.%s-cert.pem", peerDomain))
peerCert, err := os.ReadFile(peerCertPath)
Expect(err).NotTo(HaveOccurred())

renewedCert, _ := expireCertificate(peerCert, peerCACert, peerCAKey, expirationTime)
err = os.WriteFile(peerCertPath, renewedCert, 0o600)
Expect(err).NotTo(HaveOccurred())
}

// expireCertificate re-creates and re-signs a certificate with a new expirationTime
func expireCertificate(certPEM, caCertPEM, caKeyPEM []byte, expirationTime time.Time) (expiredcertPEM []byte, earlyMadeCACertPEM []byte) {
keyAsDER, _ := pem.Decode(caKeyPEM)
caKeyWithoutType, err := x509.ParsePKCS8PrivateKey(keyAsDER.Bytes)
Expect(err).NotTo(HaveOccurred())
caKey := caKeyWithoutType.(*ecdsa.PrivateKey)

caCertAsDER, _ := pem.Decode(caCertPEM)
caCert, err := x509.ParseCertificate(caCertAsDER.Bytes)
Expect(err).NotTo(HaveOccurred())

certAsDER, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certAsDER.Bytes)
Expect(err).NotTo(HaveOccurred())

cert.Raw = nil
caCert.Raw = nil
// The certificate was made 1 minute ago (1 hour doesn't work since cert will be before original CA cert NotBefore time)
cert.NotBefore = time.Now().Add((-1) * time.Minute)
// As well as the CA certificate
caCert.NotBefore = time.Now().Add((-1) * time.Minute)
// The certificate expires now
cert.NotAfter = expirationTime

// The CA creates and signs a temporary certificate
tempCertBytes, err := x509.CreateCertificate(rand.Reader, cert, caCert, cert.PublicKey, caKey)
Expect(err).NotTo(HaveOccurred())

// Force the certificate to use Low-S signature to be compatible with the identities that Fabric uses

// Parse the certificate to extract the TBS (to-be-signed) data
tempParsedCert, err := x509.ParseCertificate(tempCertBytes)
Expect(err).NotTo(HaveOccurred())

// Hash the TBS data
hash := sha256.Sum256(tempParsedCert.RawTBSCertificate)

// Sign the hash using forceLowS
r, s, err := forceLowS(caKey, hash[:])
Expect(err).NotTo(HaveOccurred())

// Encode the signature (DER format)
signature := append(r.Bytes(), s.Bytes()...)

// Replace the signature in the certificate with the low-s signature
tempParsedCert.Signature = signature

// Re-encode the certificate with the low-s signature
certBytes, err := x509.CreateCertificate(rand.Reader, tempParsedCert, caCert, cert.PublicKey, caKey)
Expect(err).NotTo(HaveOccurred())

// The CA signs its own certificate
caCertBytes, err := x509.CreateCertificate(rand.Reader, caCert, caCert, caCert.PublicKey, caKey)
Expect(err).NotTo(HaveOccurred())

expiredcertPEM = pem.EncodeToMemory(&pem.Block{Bytes: certBytes, Type: "CERTIFICATE"})
earlyMadeCACertPEM = pem.EncodeToMemory(&pem.Block{Bytes: caCertBytes, Type: "CERTIFICATE"})
return
}

// forceLowS ensures the ECDSA signature's S value is low
func forceLowS(priv *ecdsa.PrivateKey, hash []byte) (r, s *big.Int, err error) {
r, s, err = ecdsa.Sign(rand.Reader, priv, hash)
Expect(err).NotTo(HaveOccurred())

curveOrder := priv.Curve.Params().N
halfOrder := new(big.Int).Rsh(curveOrder, 1) // curveOrder / 2

// If s is greater than half the order, replace it with curveOrder - s
if s.Cmp(halfOrder) > 0 {
s.Sub(curveOrder, s)
}

return r, s, nil
}

func runTransactions(n *nwo.Network, orderer *nwo.Orderer, peer *nwo.Peer, chaincodeName string, channelID string) {
for i := 0; i < 5; i++ {
sess, err := n.PeerUserSession(peer, "User1", commands.ChaincodeInvoke{
Expand Down