-
Notifications
You must be signed in to change notification settings - Fork 8.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[FAB-7631] Block expired x509 identities in deliver
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
Showing
13 changed files
with
392 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.