|
7 | 7 | package gossip |
8 | 8 |
|
9 | 9 | import ( |
| 10 | + "crypto/ecdsa" |
| 11 | + "crypto/rand" |
| 12 | + "crypto/sha256" |
| 13 | + "crypto/x509" |
| 14 | + "encoding/pem" |
10 | 15 | "fmt" |
| 16 | + "math/big" |
11 | 17 | "os" |
12 | 18 | "path/filepath" |
13 | 19 | "syscall" |
@@ -285,8 +291,169 @@ var _ = Describe("Gossip State Transfer and Membership", func() { |
285 | 291 | assertPeerMembershipUpdate(network, peer1Org1, []*nwo.Peer{peer0Org2, peer1Org2}, nwprocs, expectedMsgFromExpirationCallback) |
286 | 292 | }) |
287 | 293 | }) |
| 294 | + |
| 295 | + It("updates membership for a peer with a renewed certificate", func() { |
| 296 | + network.Bootstrap() |
| 297 | + orderer := network.Orderer("orderer") |
| 298 | + nwprocs.ordererRunner = network.OrdererRunner(orderer) |
| 299 | + nwprocs.ordererProcess = ifrit.Invoke(nwprocs.ordererRunner) |
| 300 | + Eventually(nwprocs.ordererProcess.Ready(), network.EventuallyTimeout).Should(BeClosed()) |
| 301 | + |
| 302 | + peer0Org1 := network.Peer("Org1", "peer0") |
| 303 | + peer0Org2 := network.Peer("Org2", "peer0") |
| 304 | + |
| 305 | + By("bringing up a peer in each organization") |
| 306 | + startPeers(nwprocs, false, peer0Org1, peer0Org2) |
| 307 | + |
| 308 | + channelparticipation.JoinOrdererAppChannel(network, "testchannel", orderer, nwprocs.ordererRunner) |
| 309 | + |
| 310 | + By("joining peers to channel") |
| 311 | + network.JoinChannel(channelName, orderer, peer0Org1, peer0Org2) |
| 312 | + |
| 313 | + By("verifying membership of both peers") |
| 314 | + Eventually(nwo.DiscoverPeers(network, peer0Org1, "User1", "testchannel"), 50*time.Second, 100*time.Millisecond).Should(ContainElements(network.DiscoveredPeer(peer0Org2, "_lifecycle"))) |
| 315 | + |
| 316 | + time.Sleep(5 * time.Second) |
| 317 | + |
| 318 | + By("stopping, renewing peer0Org2 certificate before expiration, and restarting") |
| 319 | + stopPeers(nwprocs, peer0Org2) |
| 320 | + renewPeerCertificate(network, peer0Org2, time.Now().Add(time.Minute)) |
| 321 | + |
| 322 | + time.Sleep(5 * time.Second) |
| 323 | + |
| 324 | + startPeers(nwprocs, false, peer0Org2) |
| 325 | + |
| 326 | + By("ensuring that peer0Org1 replaces peer0Org2 PKI-ID") |
| 327 | + peer0Org1Runner := nwprocs.peerRunners[peer0Org1.ID()] |
| 328 | + Eventually(peer0Org1Runner.Err(), network.EventuallyTimeout).Should(gbytes.Say("changed its PKI-ID from")) |
| 329 | + |
| 330 | + time.Sleep(5 * time.Second) |
| 331 | + |
| 332 | + By("verifying membership after cert renewed") |
| 333 | + Eventually( |
| 334 | + nwo.DiscoverPeers(network, peer0Org1, "User1", "testchannel"), |
| 335 | + 60*time.Second, |
| 336 | + 100*time.Millisecond). |
| 337 | + Should(ContainElements(network.DiscoveredPeer(network.Peer("Org2", "peer0"), "_lifecycle"))) |
| 338 | + |
| 339 | + time.Sleep(5 * time.Second) |
| 340 | + |
| 341 | + By("waiting for cert to expire within a minute") |
| 342 | + Eventually(peer0Org1Runner.Err(), network.EventuallyTimeout).Should(gbytes.Say("gossipping peer identity expired")) |
| 343 | + |
| 344 | + By("stopping, renewing peer0Org2 certificate again after its expiration, restarting") |
| 345 | + stopPeers(nwprocs, peer0Org2) |
| 346 | + renewPeerCertificate(network, peer0Org2, time.Now().Add(time.Hour)) |
| 347 | + |
| 348 | + time.Sleep(5 * time.Second) |
| 349 | + |
| 350 | + startPeers(nwprocs, false, peer0Org2) |
| 351 | + |
| 352 | + Eventually( |
| 353 | + nwo.DiscoverPeers(network, peer0Org1, "User1", "testchannel"), |
| 354 | + 60*time.Second, |
| 355 | + 100*time.Millisecond). |
| 356 | + Should(ContainElements(network.DiscoveredPeer(network.Peer("Org2", "peer0"), "_lifecycle"))) |
| 357 | + }) |
288 | 358 | }) |
289 | 359 |
|
| 360 | +// renewPeerCertificate renews the certificate with a given expirationTime and re-writes it to the peer's signcert directory |
| 361 | +func renewPeerCertificate(network *nwo.Network, peer *nwo.Peer, expirationTime time.Time) { |
| 362 | + peerDomain := network.Organization(peer.Organization).Domain |
| 363 | + |
| 364 | + peerCAKeyPath := filepath.Join(network.RootDir, "crypto", "peerOrganizations", peerDomain, "ca", "priv_sk") |
| 365 | + peerCAKey, err := os.ReadFile(peerCAKeyPath) |
| 366 | + Expect(err).NotTo(HaveOccurred()) |
| 367 | + |
| 368 | + peerCACertPath := filepath.Join(network.RootDir, "crypto", "peerOrganizations", peerDomain, "ca", fmt.Sprintf("ca.%s-cert.pem", peerDomain)) |
| 369 | + peerCACert, err := os.ReadFile(peerCACertPath) |
| 370 | + Expect(err).NotTo(HaveOccurred()) |
| 371 | + |
| 372 | + peerCertPath := filepath.Join(network.PeerLocalMSPDir(peer), "signcerts", fmt.Sprintf("peer0.%s-cert.pem", peerDomain)) |
| 373 | + peerCert, err := os.ReadFile(peerCertPath) |
| 374 | + Expect(err).NotTo(HaveOccurred()) |
| 375 | + |
| 376 | + renewedCert, _ := expireCertificate(peerCert, peerCACert, peerCAKey, expirationTime) |
| 377 | + err = os.WriteFile(peerCertPath, renewedCert, 0o600) |
| 378 | + Expect(err).NotTo(HaveOccurred()) |
| 379 | +} |
| 380 | + |
| 381 | +// expireCertificate re-creates and re-signs a certificate with a new expirationTime |
| 382 | +func expireCertificate(certPEM, caCertPEM, caKeyPEM []byte, expirationTime time.Time) (expiredcertPEM []byte, earlyMadeCACertPEM []byte) { |
| 383 | + keyAsDER, _ := pem.Decode(caKeyPEM) |
| 384 | + caKeyWithoutType, err := x509.ParsePKCS8PrivateKey(keyAsDER.Bytes) |
| 385 | + Expect(err).NotTo(HaveOccurred()) |
| 386 | + caKey := caKeyWithoutType.(*ecdsa.PrivateKey) |
| 387 | + |
| 388 | + caCertAsDER, _ := pem.Decode(caCertPEM) |
| 389 | + caCert, err := x509.ParseCertificate(caCertAsDER.Bytes) |
| 390 | + Expect(err).NotTo(HaveOccurred()) |
| 391 | + |
| 392 | + certAsDER, _ := pem.Decode(certPEM) |
| 393 | + cert, err := x509.ParseCertificate(certAsDER.Bytes) |
| 394 | + Expect(err).NotTo(HaveOccurred()) |
| 395 | + |
| 396 | + cert.Raw = nil |
| 397 | + caCert.Raw = nil |
| 398 | + // The certificate was made 1 minute ago (1 hour doesn't work since cert will be before original CA cert NotBefore time) |
| 399 | + cert.NotBefore = time.Now().Add((-1) * time.Minute) |
| 400 | + // As well as the CA certificate |
| 401 | + caCert.NotBefore = time.Now().Add((-1) * time.Minute) |
| 402 | + // The certificate expires now |
| 403 | + cert.NotAfter = expirationTime |
| 404 | + |
| 405 | + // The CA creates and signs a temporary certificate |
| 406 | + tempCertBytes, err := x509.CreateCertificate(rand.Reader, cert, caCert, cert.PublicKey, caKey) |
| 407 | + Expect(err).NotTo(HaveOccurred()) |
| 408 | + |
| 409 | + // Force the certificate to use Low-S signature to be compatible with the identities that Fabric uses |
| 410 | + |
| 411 | + // Parse the certificate to extract the TBS (to-be-signed) data |
| 412 | + tempParsedCert, err := x509.ParseCertificate(tempCertBytes) |
| 413 | + Expect(err).NotTo(HaveOccurred()) |
| 414 | + |
| 415 | + // Hash the TBS data |
| 416 | + hash := sha256.Sum256(tempParsedCert.RawTBSCertificate) |
| 417 | + |
| 418 | + // Sign the hash using forceLowS |
| 419 | + r, s, err := forceLowS(caKey, hash[:]) |
| 420 | + Expect(err).NotTo(HaveOccurred()) |
| 421 | + |
| 422 | + // Encode the signature (DER format) |
| 423 | + signature := append(r.Bytes(), s.Bytes()...) |
| 424 | + |
| 425 | + // Replace the signature in the certificate with the low-s signature |
| 426 | + tempParsedCert.Signature = signature |
| 427 | + |
| 428 | + // Re-encode the certificate with the low-s signature |
| 429 | + certBytes, err := x509.CreateCertificate(rand.Reader, tempParsedCert, caCert, cert.PublicKey, caKey) |
| 430 | + Expect(err).NotTo(HaveOccurred()) |
| 431 | + |
| 432 | + // The CA signs its own certificate |
| 433 | + caCertBytes, err := x509.CreateCertificate(rand.Reader, caCert, caCert, caCert.PublicKey, caKey) |
| 434 | + Expect(err).NotTo(HaveOccurred()) |
| 435 | + |
| 436 | + expiredcertPEM = pem.EncodeToMemory(&pem.Block{Bytes: certBytes, Type: "CERTIFICATE"}) |
| 437 | + earlyMadeCACertPEM = pem.EncodeToMemory(&pem.Block{Bytes: caCertBytes, Type: "CERTIFICATE"}) |
| 438 | + return |
| 439 | +} |
| 440 | + |
| 441 | +// forceLowS ensures the ECDSA signature's S value is low |
| 442 | +func forceLowS(priv *ecdsa.PrivateKey, hash []byte) (r, s *big.Int, err error) { |
| 443 | + r, s, err = ecdsa.Sign(rand.Reader, priv, hash) |
| 444 | + Expect(err).NotTo(HaveOccurred()) |
| 445 | + |
| 446 | + curveOrder := priv.Curve.Params().N |
| 447 | + halfOrder := new(big.Int).Rsh(curveOrder, 1) // curveOrder / 2 |
| 448 | + |
| 449 | + // If s is greater than half the order, replace it with curveOrder - s |
| 450 | + if s.Cmp(halfOrder) > 0 { |
| 451 | + s.Sub(curveOrder, s) |
| 452 | + } |
| 453 | + |
| 454 | + return r, s, nil |
| 455 | +} |
| 456 | + |
290 | 457 | func runTransactions(n *nwo.Network, orderer *nwo.Orderer, peer *nwo.Peer, chaincodeName string, channelID string) { |
291 | 458 | for i := 0; i < 5; i++ { |
292 | 459 | sess, err := n.PeerUserSession(peer, "User1", commands.ChaincodeInvoke{ |
|
0 commit comments