Skip to content

Commit 665ace6

Browse files
committed
[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>
1 parent 90b9269 commit 665ace6

File tree

13 files changed

+392
-28
lines changed

13 files changed

+392
-28
lines changed

common/crypto/expiration.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package crypto
8+
9+
import (
10+
"crypto/x509"
11+
"encoding/pem"
12+
"time"
13+
14+
"github.com/golang/protobuf/proto"
15+
"github.com/hyperledger/fabric/protos/msp"
16+
)
17+
18+
// ExpiresAt returns when the given identity expires, or a zero time.Time
19+
// in case we cannot determine that
20+
func ExpiresAt(identityBytes []byte) time.Time {
21+
sId := &msp.SerializedIdentity{}
22+
// If protobuf parsing failed, we make no decisions about the expiration time
23+
if err := proto.Unmarshal(identityBytes, sId); err != nil {
24+
return time.Time{}
25+
}
26+
bl, _ := pem.Decode(sId.IdBytes)
27+
if bl == nil {
28+
// If the identity isn't a PEM block, we make no decisions about the expiration time
29+
return time.Time{}
30+
}
31+
cert, err := x509.ParseCertificate(bl.Bytes)
32+
if err != nil {
33+
return time.Time{}
34+
}
35+
return cert.NotAfter
36+
}

common/crypto/expiration_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package crypto
8+
9+
import (
10+
"io/ioutil"
11+
"path/filepath"
12+
"testing"
13+
"time"
14+
15+
"github.com/golang/protobuf/proto"
16+
"github.com/hyperledger/fabric/protos/msp"
17+
"github.com/stretchr/testify/assert"
18+
)
19+
20+
func TestX509CertExpiresAt(t *testing.T) {
21+
certBytes, err := ioutil.ReadFile(filepath.Join("testdata", "cert.pem"))
22+
assert.NoError(t, err)
23+
sId := &msp.SerializedIdentity{
24+
IdBytes: certBytes,
25+
}
26+
serializedIdentity, err := proto.Marshal(sId)
27+
assert.NoError(t, err)
28+
expirationTime := ExpiresAt(serializedIdentity)
29+
assert.Equal(t, time.Date(2027, 8, 17, 12, 19, 48, 0, time.UTC), expirationTime)
30+
}
31+
32+
func TestX509InvalidCertExpiresAt(t *testing.T) {
33+
certBytes, err := ioutil.ReadFile(filepath.Join("testdata", "badCert.pem"))
34+
assert.NoError(t, err)
35+
sId := &msp.SerializedIdentity{
36+
IdBytes: certBytes,
37+
}
38+
serializedIdentity, err := proto.Marshal(sId)
39+
assert.NoError(t, err)
40+
expirationTime := ExpiresAt(serializedIdentity)
41+
assert.True(t, expirationTime.IsZero())
42+
}
43+
44+
func TestIdemixIdentityExpiresAt(t *testing.T) {
45+
idemixId := &msp.SerializedIdemixIdentity{
46+
NymX: []byte{1, 2, 3},
47+
NymY: []byte{1, 2, 3},
48+
OU: []byte("OU1"),
49+
}
50+
idemixBytes, err := proto.Marshal(idemixId)
51+
assert.NoError(t, err)
52+
sId := &msp.SerializedIdentity{
53+
IdBytes: idemixBytes,
54+
}
55+
serializedIdentity, err := proto.Marshal(sId)
56+
assert.NoError(t, err)
57+
expirationTime := ExpiresAt(serializedIdentity)
58+
assert.True(t, expirationTime.IsZero())
59+
}
60+
61+
func TestInvalidIdentityExpiresAt(t *testing.T) {
62+
expirationTime := ExpiresAt([]byte{1, 2, 3})
63+
assert.True(t, expirationTime.IsZero())
64+
}

common/crypto/testdata/badCert.pem

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICCDCCAa6gAwIBAgIRANLH5Ue5a6tHuzCQtap1BP8wCgYIKoZIzj0EAwIwZzEL
3+
MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG
4+
cmFuY2lzY28xEzARBgNVBAoTCmhybC5pYm0uaWwxFjAUBgNVBAMTDWNhLmhybC5p
5+
EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNj
6+
bzEZMBcGA1UEAwwQVXNlcjFAaHJsLmlibS5pbDBZMBMGByqGSM49AgEGCCqGSM49
7+
AwEHA0IABE7fF65KsF0nxNgIBFVA2x/QU0LuAyuTsRaSWc/ycQAuLQfCti5bYp4W
8+
WaQUc5sBaKAmVbFQTm9RhmOhtIz7PL6jTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNV
9+
HRMBAf8EAjAAMCsGA1UdIwQkMCKAIMjiBsyFZlbO6pRxo7VgoqKhl78Ujd9sdWUk
10+
epB05fodMAoGCCqGSM49BAMCA0gAMEUCIQCiOzbaApF46NVobwh3wqHf8ID1zxja
11+
j23HPXR3FjjFZgIgXLujyDGETptNrELaytjG+dxO3Kzq/SM07K2zPUg4368=
12+
-----END CERTIFICATE-----

common/crypto/testdata/cert.pem

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICCDCCAa6gAwIBAgIRANLH5Ue5a6tHuzCQtap1BP8wCgYIKoZIzj0EAwIwZzEL
3+
MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG
4+
cmFuY2lzY28xEzARBgNVBAoTCmhybC5pYm0uaWwxFjAUBgNVBAMTDWNhLmhybC5p
5+
Ym0uaWwwHhcNMTcwODE5MTIxOTQ4WhcNMjcwODE3MTIxOTQ4WjBVMQswCQYDVQQG
6+
EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNj
7+
bzEZMBcGA1UEAwwQVXNlcjFAaHJsLmlibS5pbDBZMBMGByqGSM49AgEGCCqGSM49
8+
AwEHA0IABE7fF65KsF0nxNgIBFVA2x/QU0LuAyuTsRaSWc/ycQAuLQfCti5bYp4W
9+
WaQUc5sBaKAmVbFQTm9RhmOhtIz7PL6jTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNV
10+
HRMBAf8EAjAAMCsGA1UdIwQkMCKAIMjiBsyFZlbO6pRxo7VgoqKhl78Ujd9sdWUk
11+
epB05fodMAoGCCqGSM49BAMCA0gAMEUCIQCiOzbaApF46NVobwh3wqHf8ID1zxja
12+
j23HPXR3FjjFZgIgXLujyDGETptNrELaytjG+dxO3Kzq/SM07K2zPUg4368=
13+
-----END CERTIFICATE-----

common/deliver/acl.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package deliver
8+
9+
import (
10+
"time"
11+
12+
"github.com/hyperledger/fabric/protos/common"
13+
"github.com/pkg/errors"
14+
)
15+
16+
type expiresAtFunc func(identityBytes []byte) time.Time
17+
18+
// acSupport provides the backing resources needed to support access control validation
19+
type acSupport interface {
20+
// Sequence returns the current config sequence number, can be used to detect config changes
21+
Sequence() uint64
22+
}
23+
24+
func newSessionAC(sup acSupport, env *common.Envelope, poliyChecker PolicyChecker, channel string, expiresAt expiresAtFunc) (*sessionAC, error) {
25+
signedData, err := env.AsSignedData()
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
return &sessionAC{
31+
env: env,
32+
channel: channel,
33+
acSupport: sup,
34+
checkPolicy: poliyChecker,
35+
sessionEndTime: expiresAt(signedData[0].Identity),
36+
}, nil
37+
}
38+
39+
type sessionAC struct {
40+
acSupport
41+
checkPolicy PolicyChecker
42+
channel string
43+
env *common.Envelope
44+
lastConfigSequence uint64
45+
sessionEndTime time.Time
46+
usedAtLeastOnce bool
47+
}
48+
49+
func (ac *sessionAC) evaluate() error {
50+
if !ac.sessionEndTime.IsZero() && time.Now().After(ac.sessionEndTime) {
51+
return errors.Errorf("client identity expired %v before", time.Since(ac.sessionEndTime))
52+
}
53+
54+
policyCheckNeeded := !ac.usedAtLeastOnce
55+
56+
if currentConfigSequence := ac.Sequence(); currentConfigSequence > ac.lastConfigSequence {
57+
ac.lastConfigSequence = currentConfigSequence
58+
policyCheckNeeded = true
59+
}
60+
61+
if !policyCheckNeeded {
62+
return nil
63+
}
64+
65+
ac.usedAtLeastOnce = true
66+
return ac.checkPolicy(ac.env, ac.channel)
67+
68+
}

common/deliver/acl_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package deliver
8+
9+
import (
10+
"testing"
11+
"time"
12+
13+
"github.com/hyperledger/fabric/protos/common"
14+
"github.com/hyperledger/fabric/protos/utils"
15+
"github.com/pkg/errors"
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/mock"
18+
)
19+
20+
type mockACSupport struct {
21+
mock.Mock
22+
}
23+
24+
func (s *mockACSupport) ExpiresAt(identityBytes []byte) time.Time {
25+
return s.Called().Get(0).(time.Time)
26+
}
27+
28+
func (s *mockACSupport) Sequence() uint64 {
29+
return s.Called().Get(0).(uint64)
30+
}
31+
32+
func createEnvelope() *common.Envelope {
33+
chHdr := utils.MakeChannelHeader(common.HeaderType_DELIVER_SEEK_INFO, 0, "mychannel", 0)
34+
siHdr := utils.MakeSignatureHeader(nil, nil)
35+
paylBytes := utils.MarshalOrPanic(&common.Payload{
36+
Header: utils.MakePayloadHeader(chHdr, siHdr),
37+
})
38+
39+
return &common.Envelope{Payload: paylBytes}
40+
}
41+
42+
type oneTimeInvoke struct {
43+
f func(*common.Envelope, string) error
44+
invoked bool
45+
}
46+
47+
func (oti *oneTimeInvoke) invokeOnce() func(*common.Envelope, string) error {
48+
return func(env *common.Envelope, s string) error {
49+
if oti.invoked {
50+
panic("already invoked!")
51+
}
52+
oti.invoked = true
53+
return oti.f(env, s)
54+
}
55+
}
56+
57+
func oneTimeFunction(f func(*common.Envelope, string) error) func(*common.Envelope, string) error {
58+
oti := &oneTimeInvoke{f: f}
59+
return oti.invokeOnce()
60+
}
61+
62+
func TestOneTimeFunction(t *testing.T) {
63+
acceptPolicyChecker := func(envelope *common.Envelope, channelID string) error {
64+
return nil
65+
}
66+
f := oneTimeFunction(acceptPolicyChecker)
67+
// First time no panic
68+
assert.NotPanics(t, func() {
69+
f(nil, "")
70+
})
71+
72+
// Second time we panic
73+
assert.Panics(t, func() {
74+
f(nil, "")
75+
})
76+
}
77+
78+
func TestAC(t *testing.T) {
79+
acceptPolicyChecker := func(envelope *common.Envelope, channelID string) error {
80+
return nil
81+
}
82+
83+
denyPolicyChecker := func(envelope *common.Envelope, channelID string) error {
84+
return errors.New("forbidden")
85+
}
86+
87+
sup := &mockACSupport{}
88+
// Scenario I: create empty header
89+
ac, err := newSessionAC(sup, &common.Envelope{}, nil, "mychannel", sup.ExpiresAt)
90+
assert.Nil(t, ac)
91+
assert.Contains(t, err.Error(), "Missing Header")
92+
93+
// Scenario II: Identity has expired.
94+
sup = &mockACSupport{}
95+
sup.On("ExpiresAt").Return(time.Now().Add(-1 * time.Second)).Once()
96+
ac, err = newSessionAC(sup, createEnvelope(), oneTimeFunction(acceptPolicyChecker), "mychannel", sup.ExpiresAt)
97+
assert.NotNil(t, ac)
98+
assert.NoError(t, err)
99+
err = ac.evaluate()
100+
assert.Contains(t, err.Error(), "expired")
101+
102+
// Scenario III: Identity hasn't expired, but is forbidden
103+
sup = &mockACSupport{}
104+
sup.On("ExpiresAt").Return(time.Now().Add(time.Second)).Once()
105+
sup.On("Sequence").Return(uint64(0)).Once()
106+
ac, err = newSessionAC(sup, createEnvelope(), oneTimeFunction(denyPolicyChecker), "mychannel", sup.ExpiresAt)
107+
assert.NoError(t, err)
108+
err = ac.evaluate()
109+
assert.Contains(t, err.Error(), "forbidden")
110+
111+
// Scenario IV: Identity hasn't expired, and is allowed
112+
// We actually check 2 sub-cases, the first one is if the identity can expire,
113+
// and the second one is if the identity can't expire (i.e an idemix identity currently can't expire)
114+
for _, expirationTime := range []time.Time{time.Now().Add(time.Second), {}} {
115+
sup = &mockACSupport{}
116+
sup.On("ExpiresAt").Return(expirationTime).Once()
117+
sup.On("Sequence").Return(uint64(0)).Once()
118+
ac, err = newSessionAC(sup, createEnvelope(), oneTimeFunction(acceptPolicyChecker), "mychannel", sup.ExpiresAt)
119+
assert.NoError(t, err)
120+
err = ac.evaluate()
121+
assert.NoError(t, err)
122+
// Execute again. We should not evaluate the policy again.
123+
// If we do, the test fails with panic because the function can be invoked only once
124+
sup.On("Sequence").Return(uint64(0)).Once()
125+
err = ac.evaluate()
126+
assert.NoError(t, err)
127+
}
128+
129+
// Scenario V: Identity hasn't expired, and is allowed at first, but afterwards there
130+
// is a config change and afterwards it isn't allowed
131+
sup = &mockACSupport{}
132+
sup.On("ExpiresAt").Return(time.Now().Add(time.Second)).Once()
133+
sup.On("Sequence").Return(uint64(0)).Once()
134+
sup.On("Sequence").Return(uint64(1)).Once()
135+
136+
firstInvoke := true
137+
policyChecker := func(envelope *common.Envelope, channelID string) error {
138+
if firstInvoke {
139+
firstInvoke = false
140+
return nil
141+
}
142+
return errors.New("forbidden")
143+
}
144+
145+
ac, err = newSessionAC(sup, createEnvelope(), policyChecker, "mychannel", sup.ExpiresAt)
146+
assert.NoError(t, err)
147+
err = ac.evaluate() // first time
148+
assert.NoError(t, err)
149+
err = ac.evaluate() // second time
150+
assert.Contains(t, err.Error(), "forbidden")
151+
152+
// Scenario VI: Identity hasn't expired at first, but expires at a later time,
153+
// and then it shouldn't be allowed to be serviced
154+
sup = &mockACSupport{}
155+
sup.On("ExpiresAt").Return(time.Now().Add(time.Millisecond * 500)).Once()
156+
sup.On("Sequence").Return(uint64(0)).Times(3)
157+
ac, err = newSessionAC(sup, createEnvelope(), oneTimeFunction(acceptPolicyChecker), "mychannel", sup.ExpiresAt)
158+
assert.NoError(t, err)
159+
err = ac.evaluate()
160+
assert.NoError(t, err)
161+
err = ac.evaluate()
162+
assert.NoError(t, err)
163+
time.Sleep(time.Second)
164+
err = ac.evaluate()
165+
assert.Error(t, err)
166+
assert.Contains(t, err.Error(), "expired")
167+
}

0 commit comments

Comments
 (0)