Skip to content

Commit

Permalink
Merge "[FAB-17000] Warn when cert expiration is nigh"
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 9b266bd + d7c5830 commit 2332eea
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 5 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)
})
}
118 changes: 118 additions & 0 deletions common/crypto/expiration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ SPDX-License-Identifier: Apache-2.0
package crypto

import (
"fmt"
"io/ioutil"
"path/filepath"
"testing"
"time"

"github.com/golang/protobuf/proto"
"github.com/hyperledger/fabric-protos-go/msp"
"github.com/hyperledger/fabric/common/crypto/tlsgen"
"github.com/hyperledger/fabric/protoutil"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -62,3 +65,118 @@ func TestInvalidIdentityExpiresAt(t *testing.T) {
expirationTime := 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()
expirationTime := certExpirationTime(ca.CertBytes())

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 := protoutil.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 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
}

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)
}
})
}
}
17 changes: 17 additions & 0 deletions internal/peer/node/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/hyperledger/fabric/bccsp/factory"
"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 @@ -278,6 +279,22 @@ func serve(args []string) error {
if err != nil {
logger.Panicf("Could not get the default signing identity from the local MSP: [%+v]", err)
}

signingIdentityBytes, err := signingIdentity.Serialize()
if err != nil {
logger.Panicf("Failed to serialize the signing identity: %v", err)
}

// Log warnings if some certificate (e-cert, TLS certs) expires soon
crypto.TrackExpiration(
serverConfig.SecOpts.UseTLS,
serverConfig.SecOpts.Certificate,
cs.GetClientCertificate().Certificate,
signingIdentityBytes,
logger.Warnf, // This can be used to piggyback a metric event in the future
time.Now(),
time.AfterFunc)

policyMgr := policies.PolicyManagerGetterFunc(peerInstance.GetPolicyManager)

deliverGRPCClient, err := comm.NewGRPCClient(comm.ClientConfig{
Expand Down
22 changes: 18 additions & 4 deletions orderer/common/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,13 @@ import (
"time"

"github.com/golang/protobuf/proto"
"go.uber.org/zap/zapcore"
"google.golang.org/grpc"
"gopkg.in/alecthomas/kingpin.v2"

"github.com/hyperledger/fabric-lib-go/healthz"
cb "github.com/hyperledger/fabric-protos-go/common"
ab "github.com/hyperledger/fabric-protos-go/orderer"
"github.com/hyperledger/fabric/bccsp"
"github.com/hyperledger/fabric/bccsp/factory"
"github.com/hyperledger/fabric/common/channelconfig"
"github.com/hyperledger/fabric/common/crypto"
"github.com/hyperledger/fabric/common/flogging"
floggingmetrics "github.com/hyperledger/fabric/common/flogging/metrics"
"github.com/hyperledger/fabric/common/grpclogging"
Expand All @@ -53,6 +50,9 @@ import (
"github.com/hyperledger/fabric/orderer/consensus/kafka"
"github.com/hyperledger/fabric/orderer/consensus/solo"
"github.com/hyperledger/fabric/protoutil"
"go.uber.org/zap/zapcore"
"google.golang.org/grpc"
"gopkg.in/alecthomas/kingpin.v2"
)

var logger = flogging.MustGetLogger("orderer.common.server")
Expand Down Expand Up @@ -158,6 +158,20 @@ func Main() {
serversToUpdate = append(serversToUpdate, clusterGRPCServer)
}

identityBytes, err := signer.Serialize()
if err != nil {
logger.Panicf("Failed serializing signing identity: %v", err)
}

crypto.TrackExpiration(
serverConfig.SecOpts.UseTLS,
serverConfig.SecOpts.Certificate,
[][]byte{clusterClientConfig.SecOpts.Certificate},
identityBytes,
logger.Warnf, // This can be used to piggyback a metric event in the future
time.Now(),
time.AfterFunc)

// if cluster is reusing client-facing server, then it is already
// appended to serversToUpdate at this point.
if grpcServer.MutualTLSRequired() && !reuseGrpcListener {
Expand Down

0 comments on commit 2332eea

Please sign in to comment.