Skip to content

Commit

Permalink
[FAB-7631] Block expired x509 identities in deliver
Browse files Browse the repository at this point in the history
This change set blocks expired x509 certificates from calling deliver
and also blocks them when they expire.

Change-Id: I146b9e6d1feabe66a756f73f95ab962cf39d3ec5
Signed-off-by: yacovm <yacovm@il.ibm.com>
  • Loading branch information
yacovm committed Jan 6, 2018
1 parent 90b9269 commit 665ace6
Show file tree
Hide file tree
Showing 13 changed files with 392 additions and 28 deletions.
36 changes: 36 additions & 0 deletions common/crypto/expiration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package crypto

import (
"crypto/x509"
"encoding/pem"
"time"

"github.com/golang/protobuf/proto"
"github.com/hyperledger/fabric/protos/msp"
)

// ExpiresAt returns when the given identity expires, or a zero time.Time
// in case we cannot determine that
func ExpiresAt(identityBytes []byte) time.Time {
sId := &msp.SerializedIdentity{}
// If protobuf parsing failed, we make no decisions about the expiration time
if err := proto.Unmarshal(identityBytes, sId); err != nil {
return time.Time{}
}
bl, _ := pem.Decode(sId.IdBytes)
if bl == nil {
// If the identity isn't a PEM block, we make no decisions about the expiration time
return time.Time{}
}
cert, err := x509.ParseCertificate(bl.Bytes)
if err != nil {
return time.Time{}
}
return cert.NotAfter
}
64 changes: 64 additions & 0 deletions common/crypto/expiration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package crypto

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

"github.com/golang/protobuf/proto"
"github.com/hyperledger/fabric/protos/msp"
"github.com/stretchr/testify/assert"
)

func TestX509CertExpiresAt(t *testing.T) {
certBytes, err := ioutil.ReadFile(filepath.Join("testdata", "cert.pem"))
assert.NoError(t, err)
sId := &msp.SerializedIdentity{
IdBytes: certBytes,
}
serializedIdentity, err := proto.Marshal(sId)
assert.NoError(t, err)
expirationTime := ExpiresAt(serializedIdentity)
assert.Equal(t, time.Date(2027, 8, 17, 12, 19, 48, 0, time.UTC), expirationTime)
}

func TestX509InvalidCertExpiresAt(t *testing.T) {
certBytes, err := ioutil.ReadFile(filepath.Join("testdata", "badCert.pem"))
assert.NoError(t, err)
sId := &msp.SerializedIdentity{
IdBytes: certBytes,
}
serializedIdentity, err := proto.Marshal(sId)
assert.NoError(t, err)
expirationTime := ExpiresAt(serializedIdentity)
assert.True(t, expirationTime.IsZero())
}

func TestIdemixIdentityExpiresAt(t *testing.T) {
idemixId := &msp.SerializedIdemixIdentity{
NymX: []byte{1, 2, 3},
NymY: []byte{1, 2, 3},
OU: []byte("OU1"),
}
idemixBytes, err := proto.Marshal(idemixId)
assert.NoError(t, err)
sId := &msp.SerializedIdentity{
IdBytes: idemixBytes,
}
serializedIdentity, err := proto.Marshal(sId)
assert.NoError(t, err)
expirationTime := ExpiresAt(serializedIdentity)
assert.True(t, expirationTime.IsZero())
}

func TestInvalidIdentityExpiresAt(t *testing.T) {
expirationTime := ExpiresAt([]byte{1, 2, 3})
assert.True(t, expirationTime.IsZero())
}
12 changes: 12 additions & 0 deletions common/crypto/testdata/badCert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIICCDCCAa6gAwIBAgIRANLH5Ue5a6tHuzCQtap1BP8wCgYIKoZIzj0EAwIwZzEL
MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG
cmFuY2lzY28xEzARBgNVBAoTCmhybC5pYm0uaWwxFjAUBgNVBAMTDWNhLmhybC5p
EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNj
bzEZMBcGA1UEAwwQVXNlcjFAaHJsLmlibS5pbDBZMBMGByqGSM49AgEGCCqGSM49
AwEHA0IABE7fF65KsF0nxNgIBFVA2x/QU0LuAyuTsRaSWc/ycQAuLQfCti5bYp4W
WaQUc5sBaKAmVbFQTm9RhmOhtIz7PL6jTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNV
HRMBAf8EAjAAMCsGA1UdIwQkMCKAIMjiBsyFZlbO6pRxo7VgoqKhl78Ujd9sdWUk
epB05fodMAoGCCqGSM49BAMCA0gAMEUCIQCiOzbaApF46NVobwh3wqHf8ID1zxja
j23HPXR3FjjFZgIgXLujyDGETptNrELaytjG+dxO3Kzq/SM07K2zPUg4368=
-----END CERTIFICATE-----
13 changes: 13 additions & 0 deletions common/crypto/testdata/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIICCDCCAa6gAwIBAgIRANLH5Ue5a6tHuzCQtap1BP8wCgYIKoZIzj0EAwIwZzEL
MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG
cmFuY2lzY28xEzARBgNVBAoTCmhybC5pYm0uaWwxFjAUBgNVBAMTDWNhLmhybC5p
Ym0uaWwwHhcNMTcwODE5MTIxOTQ4WhcNMjcwODE3MTIxOTQ4WjBVMQswCQYDVQQG
EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNj
bzEZMBcGA1UEAwwQVXNlcjFAaHJsLmlibS5pbDBZMBMGByqGSM49AgEGCCqGSM49
AwEHA0IABE7fF65KsF0nxNgIBFVA2x/QU0LuAyuTsRaSWc/ycQAuLQfCti5bYp4W
WaQUc5sBaKAmVbFQTm9RhmOhtIz7PL6jTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNV
HRMBAf8EAjAAMCsGA1UdIwQkMCKAIMjiBsyFZlbO6pRxo7VgoqKhl78Ujd9sdWUk
epB05fodMAoGCCqGSM49BAMCA0gAMEUCIQCiOzbaApF46NVobwh3wqHf8ID1zxja
j23HPXR3FjjFZgIgXLujyDGETptNrELaytjG+dxO3Kzq/SM07K2zPUg4368=
-----END CERTIFICATE-----
68 changes: 68 additions & 0 deletions common/deliver/acl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package deliver

import (
"time"

"github.com/hyperledger/fabric/protos/common"
"github.com/pkg/errors"
)

type expiresAtFunc func(identityBytes []byte) time.Time

// acSupport provides the backing resources needed to support access control validation
type acSupport interface {
// Sequence returns the current config sequence number, can be used to detect config changes
Sequence() uint64
}

func newSessionAC(sup acSupport, env *common.Envelope, poliyChecker PolicyChecker, channel string, expiresAt expiresAtFunc) (*sessionAC, error) {
signedData, err := env.AsSignedData()
if err != nil {
return nil, err
}

return &sessionAC{
env: env,
channel: channel,
acSupport: sup,
checkPolicy: poliyChecker,
sessionEndTime: expiresAt(signedData[0].Identity),
}, nil
}

type sessionAC struct {
acSupport
checkPolicy PolicyChecker
channel string
env *common.Envelope
lastConfigSequence uint64
sessionEndTime time.Time
usedAtLeastOnce bool
}

func (ac *sessionAC) evaluate() error {
if !ac.sessionEndTime.IsZero() && time.Now().After(ac.sessionEndTime) {
return errors.Errorf("client identity expired %v before", time.Since(ac.sessionEndTime))
}

policyCheckNeeded := !ac.usedAtLeastOnce

if currentConfigSequence := ac.Sequence(); currentConfigSequence > ac.lastConfigSequence {
ac.lastConfigSequence = currentConfigSequence
policyCheckNeeded = true
}

if !policyCheckNeeded {
return nil
}

ac.usedAtLeastOnce = true
return ac.checkPolicy(ac.env, ac.channel)

}
167 changes: 167 additions & 0 deletions common/deliver/acl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package deliver

import (
"testing"
"time"

"github.com/hyperledger/fabric/protos/common"
"github.com/hyperledger/fabric/protos/utils"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

type mockACSupport struct {
mock.Mock
}

func (s *mockACSupport) ExpiresAt(identityBytes []byte) time.Time {
return s.Called().Get(0).(time.Time)
}

func (s *mockACSupport) Sequence() uint64 {
return s.Called().Get(0).(uint64)
}

func createEnvelope() *common.Envelope {
chHdr := utils.MakeChannelHeader(common.HeaderType_DELIVER_SEEK_INFO, 0, "mychannel", 0)
siHdr := utils.MakeSignatureHeader(nil, nil)
paylBytes := utils.MarshalOrPanic(&common.Payload{
Header: utils.MakePayloadHeader(chHdr, siHdr),
})

return &common.Envelope{Payload: paylBytes}
}

type oneTimeInvoke struct {
f func(*common.Envelope, string) error
invoked bool
}

func (oti *oneTimeInvoke) invokeOnce() func(*common.Envelope, string) error {
return func(env *common.Envelope, s string) error {
if oti.invoked {
panic("already invoked!")
}
oti.invoked = true
return oti.f(env, s)
}
}

func oneTimeFunction(f func(*common.Envelope, string) error) func(*common.Envelope, string) error {
oti := &oneTimeInvoke{f: f}
return oti.invokeOnce()
}

func TestOneTimeFunction(t *testing.T) {
acceptPolicyChecker := func(envelope *common.Envelope, channelID string) error {
return nil
}
f := oneTimeFunction(acceptPolicyChecker)
// First time no panic
assert.NotPanics(t, func() {
f(nil, "")
})

// Second time we panic
assert.Panics(t, func() {
f(nil, "")
})
}

func TestAC(t *testing.T) {
acceptPolicyChecker := func(envelope *common.Envelope, channelID string) error {
return nil
}

denyPolicyChecker := func(envelope *common.Envelope, channelID string) error {
return errors.New("forbidden")
}

sup := &mockACSupport{}
// Scenario I: create empty header
ac, err := newSessionAC(sup, &common.Envelope{}, nil, "mychannel", sup.ExpiresAt)
assert.Nil(t, ac)
assert.Contains(t, err.Error(), "Missing Header")

// Scenario II: Identity has expired.
sup = &mockACSupport{}
sup.On("ExpiresAt").Return(time.Now().Add(-1 * time.Second)).Once()
ac, err = newSessionAC(sup, createEnvelope(), oneTimeFunction(acceptPolicyChecker), "mychannel", sup.ExpiresAt)
assert.NotNil(t, ac)
assert.NoError(t, err)
err = ac.evaluate()
assert.Contains(t, err.Error(), "expired")

// Scenario III: Identity hasn't expired, but is forbidden
sup = &mockACSupport{}
sup.On("ExpiresAt").Return(time.Now().Add(time.Second)).Once()
sup.On("Sequence").Return(uint64(0)).Once()
ac, err = newSessionAC(sup, createEnvelope(), oneTimeFunction(denyPolicyChecker), "mychannel", sup.ExpiresAt)
assert.NoError(t, err)
err = ac.evaluate()
assert.Contains(t, err.Error(), "forbidden")

// Scenario IV: Identity hasn't expired, and is allowed
// We actually check 2 sub-cases, the first one is if the identity can expire,
// and the second one is if the identity can't expire (i.e an idemix identity currently can't expire)
for _, expirationTime := range []time.Time{time.Now().Add(time.Second), {}} {
sup = &mockACSupport{}
sup.On("ExpiresAt").Return(expirationTime).Once()
sup.On("Sequence").Return(uint64(0)).Once()
ac, err = newSessionAC(sup, createEnvelope(), oneTimeFunction(acceptPolicyChecker), "mychannel", sup.ExpiresAt)
assert.NoError(t, err)
err = ac.evaluate()
assert.NoError(t, err)
// Execute again. We should not evaluate the policy again.
// If we do, the test fails with panic because the function can be invoked only once
sup.On("Sequence").Return(uint64(0)).Once()
err = ac.evaluate()
assert.NoError(t, err)
}

// Scenario V: Identity hasn't expired, and is allowed at first, but afterwards there
// is a config change and afterwards it isn't allowed
sup = &mockACSupport{}
sup.On("ExpiresAt").Return(time.Now().Add(time.Second)).Once()
sup.On("Sequence").Return(uint64(0)).Once()
sup.On("Sequence").Return(uint64(1)).Once()

firstInvoke := true
policyChecker := func(envelope *common.Envelope, channelID string) error {
if firstInvoke {
firstInvoke = false
return nil
}
return errors.New("forbidden")
}

ac, err = newSessionAC(sup, createEnvelope(), policyChecker, "mychannel", sup.ExpiresAt)
assert.NoError(t, err)
err = ac.evaluate() // first time
assert.NoError(t, err)
err = ac.evaluate() // second time
assert.Contains(t, err.Error(), "forbidden")

// Scenario VI: Identity hasn't expired at first, but expires at a later time,
// and then it shouldn't be allowed to be serviced
sup = &mockACSupport{}
sup.On("ExpiresAt").Return(time.Now().Add(time.Millisecond * 500)).Once()
sup.On("Sequence").Return(uint64(0)).Times(3)
ac, err = newSessionAC(sup, createEnvelope(), oneTimeFunction(acceptPolicyChecker), "mychannel", sup.ExpiresAt)
assert.NoError(t, err)
err = ac.evaluate()
assert.NoError(t, err)
err = ac.evaluate()
assert.NoError(t, err)
time.Sleep(time.Second)
err = ac.evaluate()
assert.Error(t, err)
assert.Contains(t, err.Error(), "expired")
}
Loading

0 comments on commit 665ace6

Please sign in to comment.