Skip to content

Commit

Permalink
Merge "[FAB-17000] Warn when cert expiration is nigh" into release-1.4
Browse files Browse the repository at this point in the history
  • Loading branch information
yacovm authored and Gerrit Code Review committed Nov 8, 2019
2 parents 66f3d96 + 1989eee commit 872fffe
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 6 deletions.
63 changes: 62 additions & 1 deletion common/crypto/expiration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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)
})
}
134 changes: 129 additions & 5 deletions common/crypto/expiration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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)
}

Expand All @@ -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())
}

Expand All @@ -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)
}
})
}
}
15 changes: 15 additions & 0 deletions orderer/common/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions peer/node/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 872fffe

Please sign in to comment.