diff --git a/common/crypto/expiration.go b/common/crypto/expiration.go index 2055ff13032..d3d5d2b1ba3 100644 --- a/common/crypto/expiration.go +++ b/common/crypto/expiration.go @@ -23,7 +23,11 @@ func ExpiresAt(identityBytes []byte) time.Time { if err := proto.Unmarshal(identityBytes, sId); err != nil { return time.Time{} } - bl, _ := pem.Decode(sId.IdBytes) + return certExpirationTime(sId.IdBytes) +} + +func certExpirationTime(pemBytes []byte) time.Time { + bl, _ := pem.Decode(pemBytes) if bl == nil { // If the identity isn't a PEM block, we make no decisions about the expiration time return time.Time{} @@ -34,3 +38,60 @@ func ExpiresAt(identityBytes []byte) time.Time { } return cert.NotAfter } + +// WarnFunc notifies a warning happened with the given format, and can be replaced with Warnf of a logger. +type WarnFunc func(format string, args ...interface{}) + +// Scheduler invokes f after d time, and can be replaced with time.AfterFunc. +type Scheduler func(d time.Duration, f func()) *time.Timer + +// TrackExpiration warns a week before one of the certificates expires +func TrackExpiration(tls bool, serverCert []byte, clientCertChain [][]byte, sIDBytes []byte, warn WarnFunc, now time.Time, s Scheduler) { + sID := &msp.SerializedIdentity{} + if err := proto.Unmarshal(sIDBytes, sID); err != nil { + return + } + + trackCertExpiration(sID.IdBytes, "enrollment", warn, now, s) + + if !tls { + return + } + + trackCertExpiration(serverCert, "server TLS", warn, now, s) + + if len(clientCertChain) == 0 || len(clientCertChain[0]) == 0 { + return + } + + trackCertExpiration(clientCertChain[0], "client TLS", warn, now, s) +} + +func trackCertExpiration(rawCert []byte, certRole string, warn WarnFunc, now time.Time, sched Scheduler) { + expirationTime := certExpirationTime(rawCert) + if expirationTime.IsZero() { + // If the certificate expiration time cannot be classified, return. + return + } + + timeLeftUntilExpiration := expirationTime.Sub(now) + oneWeek := time.Hour * 24 * 7 + + if timeLeftUntilExpiration < 0 { + warn("The %s certificate has expired", certRole) + return + } + + if timeLeftUntilExpiration < oneWeek { + days := timeLeftUntilExpiration / (time.Hour * 24) + hours := (timeLeftUntilExpiration - (days * time.Hour * 24)) / time.Hour + warn("The %s certificate expires within %d days and %d hours", certRole, days, hours) + return + } + + timeLeftUntilOneWeekBeforeExpiration := timeLeftUntilExpiration - oneWeek + + sched(timeLeftUntilOneWeekBeforeExpiration, func() { + warn("The %s certificate will expire within one week", certRole) + }) +} diff --git a/common/crypto/expiration_test.go b/common/crypto/expiration_test.go index 7d22868d2c9..7a4a222daf0 100644 --- a/common/crypto/expiration_test.go +++ b/common/crypto/expiration_test.go @@ -4,16 +4,22 @@ Copyright IBM Corp. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package crypto +package crypto_test import ( + "crypto/x509" + "encoding/pem" + "fmt" "io/ioutil" "path/filepath" "testing" "time" "github.com/golang/protobuf/proto" + "github.com/hyperledger/fabric/common/crypto" + "github.com/hyperledger/fabric/common/crypto/tlsgen" "github.com/hyperledger/fabric/protos/msp" + "github.com/hyperledger/fabric/protos/utils" "github.com/stretchr/testify/assert" ) @@ -25,7 +31,7 @@ func TestX509CertExpiresAt(t *testing.T) { } serializedIdentity, err := proto.Marshal(sId) assert.NoError(t, err) - expirationTime := ExpiresAt(serializedIdentity) + expirationTime := crypto.ExpiresAt(serializedIdentity) assert.Equal(t, time.Date(2027, 8, 17, 12, 19, 48, 0, time.UTC), expirationTime) } @@ -37,7 +43,7 @@ func TestX509InvalidCertExpiresAt(t *testing.T) { } serializedIdentity, err := proto.Marshal(sId) assert.NoError(t, err) - expirationTime := ExpiresAt(serializedIdentity) + expirationTime := crypto.ExpiresAt(serializedIdentity) assert.True(t, expirationTime.IsZero()) } @@ -54,11 +60,129 @@ func TestIdemixIdentityExpiresAt(t *testing.T) { } serializedIdentity, err := proto.Marshal(sId) assert.NoError(t, err) - expirationTime := ExpiresAt(serializedIdentity) + expirationTime := crypto.ExpiresAt(serializedIdentity) assert.True(t, expirationTime.IsZero()) } func TestInvalidIdentityExpiresAt(t *testing.T) { - expirationTime := ExpiresAt([]byte{1, 2, 3}) + expirationTime := crypto.ExpiresAt([]byte{1, 2, 3}) assert.True(t, expirationTime.IsZero()) } + +func TestTrackExpiration(t *testing.T) { + ca, err := tlsgen.NewCA() + assert.NoError(t, err) + + now := time.Now() + bl, _ := pem.Decode(ca.CertBytes()) + caCert, err := x509.ParseCertificate(bl.Bytes) + assert.NoError(t, err) + expirationTime := caCert.NotAfter + + timeUntilExpiration := expirationTime.Sub(now) + timeUntilOneMonthBeforeExpiration := timeUntilExpiration - 28*24*time.Hour + timeUntil2DaysBeforeExpiration := timeUntilExpiration - 2*24*time.Hour - time.Hour*12 + + monthBeforeExpiration := now.Add(timeUntilOneMonthBeforeExpiration) + twoDaysBeforeExpiration := now.Add(timeUntil2DaysBeforeExpiration) + + tlsCert, err := ca.NewServerCertKeyPair("127.0.0.1") + assert.NoError(t, err) + + signingIdentity := utils.MarshalOrPanic(&msp.SerializedIdentity{ + IdBytes: tlsCert.Cert, + }) + + shouldNotBeInvoked := func(format string, args ...interface{}) { + t.Fatalf(format, args...) + } + + var formattedWarning string + shouldBeInvoked := func(format string, args ...interface{}) { + formattedWarning = fmt.Sprintf(format, args...) + } + + for _, testCase := range []struct { + description string + tls bool + serverCert []byte + clientCertChain [][]byte + sIDBytes []byte + warn crypto.WarnFunc + now time.Time + expectedWarn string + }{ + { + description: "No TLS, enrollment cert isn't valid logs a warning", + warn: shouldNotBeInvoked, + sIDBytes: []byte{1, 2, 3}, + }, + { + description: "No TLS, enrollment cert expires soon", + sIDBytes: signingIdentity, + warn: shouldBeInvoked, + now: monthBeforeExpiration, + expectedWarn: "The enrollment certificate will expire within one week", + }, + { + description: "TLS, server cert expires soon", + warn: shouldBeInvoked, + now: monthBeforeExpiration, + tls: true, + serverCert: tlsCert.Cert, + expectedWarn: "The server TLS certificate will expire within one week", + }, + { + description: "TLS, server cert expires really soon", + warn: shouldBeInvoked, + now: twoDaysBeforeExpiration, + tls: true, + serverCert: tlsCert.Cert, + expectedWarn: "The server TLS certificate expires within 2 days and 12 hours", + }, + { + description: "TLS, server cert has expired", + warn: shouldBeInvoked, + now: expirationTime.Add(time.Hour), + tls: true, + serverCert: tlsCert.Cert, + expectedWarn: "The server TLS certificate has expired", + }, + { + description: "TLS, client cert expires soon", + warn: shouldBeInvoked, + now: monthBeforeExpiration, + tls: true, + clientCertChain: [][]byte{tlsCert.Cert}, + expectedWarn: "The client TLS certificate will expire within one week", + }, + } { + t.Run(testCase.description, func(t *testing.T) { + defer func() { + formattedWarning = "" + }() + + fakeTimeAfter := func(duration time.Duration, f func()) *time.Timer { + assert.NotEmpty(t, testCase.expectedWarn) + threeWeeks := 3 * 7 * 24 * time.Hour + assert.Equal(t, threeWeeks, duration) + f() + return nil + } + + crypto.TrackExpiration(testCase.tls, + testCase.serverCert, + testCase.clientCertChain, + testCase.sIDBytes, + testCase.warn, + testCase.now, + fakeTimeAfter) + + if testCase.expectedWarn != "" { + assert.Equal(t, testCase.expectedWarn, formattedWarning) + } else { + assert.Empty(t, formattedWarning) + } + }) + } +} diff --git a/orderer/common/server/main.go b/orderer/common/server/main.go index 001d54a2363..a44ac3e6dbb 100644 --- a/orderer/common/server/main.go +++ b/orderer/common/server/main.go @@ -159,6 +159,21 @@ func Start(cmd string, conf *localconfig.TopLevel) { } } + sigHdr, err := signer.NewSignatureHeader() + if err != nil { + logger.Panicf("Failed creating a signature header: %v", err) + } + + expirationLogger := flogging.MustGetLogger("certmonitor") + crypto.TrackExpiration( + serverConfig.SecOpts.UseTLS, + serverConfig.SecOpts.Certificate, + [][]byte{clusterClientConfig.SecOpts.Certificate}, + sigHdr.Creator, + expirationLogger.Warnf, // This can be used to piggyback a metric event in the future + time.Now(), + time.AfterFunc) + manager := initializeMultichannelRegistrar(clusterBootBlock, r, clusterDialer, clusterServerConfig, clusterGRPCServer, conf, signer, metricsProvider, opsSystem, lf, tlsCallback) mutualTLS := serverConfig.SecOpts.UseTLS && serverConfig.SecOpts.RequireClientCert expiration := conf.General.Authentication.NoExpirationChecks diff --git a/peer/node/start.go b/peer/node/start.go index b691739452e..961b75a248a 100644 --- a/peer/node/start.go +++ b/peer/node/start.go @@ -20,6 +20,7 @@ import ( "github.com/golang/protobuf/proto" "github.com/hyperledger/fabric/common/cauthdsl" ccdef "github.com/hyperledger/fabric/common/chaincode" + "github.com/hyperledger/fabric/common/crypto" "github.com/hyperledger/fabric/common/crypto/tlsgen" "github.com/hyperledger/fabric/common/deliver" "github.com/hyperledger/fabric/common/flogging" @@ -314,6 +315,16 @@ func serve(args []string) error { endorserSupport.PluginEndorser = pluginEndorser serverEndorser := endorser.NewEndorserServer(privDataDist, endorserSupport, pr, metricsProvider) + expirationLogger := flogging.MustGetLogger("certmonitor") + crypto.TrackExpiration( + serverConfig.SecOpts.UseTLS, + serverConfig.SecOpts.Certificate, + comm.GetCredentialSupport().GetClientCertificate().Certificate, + serializedIdentity, + expirationLogger.Warnf, // This can be used to piggyback a metric event in the future + time.Now(), + time.AfterFunc) + policyMgr := peer.NewChannelPolicyManagerGetter() // Initialize gossip component