Skip to content

Commit f0f4c3f

Browse files
committed
[FAB-6868] Add mutualTLS bindings to channel header
The peer resource config, as well as the event service, and perhaps also the Deliver() and Broadcast() services in the ordering service could all make use of clients using mutual TLS and binding their TLS certificates to the messages they send to the service, in order for: - The messages not to be relayable to other nodes - Leverage the TLS handshake as a challenge-response phase prior to serving the client request (the client proves he still holds the private key of its TLS certificate at the time of sending the request. This change set: - Adds a TLS cert hash in the channel header - Adds functions in the comm package that check whether the Envelope the client sent is bound to the client's TLS certificate and also ensures the client uses mutual TLS. - Unit tests that provide 100% code coverage for the added production code Change-Id: I9a33b132731fcc621dd7d3a35d26fbbaf7de5c12 Signed-off-by: yacovm <yacovm@il.ibm.com>
1 parent 6cf9558 commit f0f4c3f

File tree

6 files changed

+356
-61
lines changed

6 files changed

+356
-61
lines changed

core/comm/util.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@ SPDX-License-Identifier: Apache-2.0
77
package comm
88

99
import (
10+
"bytes"
1011
"crypto/x509"
1112
"encoding/pem"
13+
14+
"github.com/hyperledger/fabric/common/util"
15+
"github.com/hyperledger/fabric/protos/common"
16+
"github.com/hyperledger/fabric/protos/utils"
17+
"github.com/pkg/errors"
18+
"golang.org/x/net/context"
19+
"google.golang.org/grpc/credentials"
20+
"google.golang.org/grpc/peer"
1221
)
1322

1423
// AddPemToCertPool adds PEM-encoded certs to a cert pool
@@ -52,3 +61,76 @@ func pemToX509Certs(pemCerts []byte) ([]*x509.Certificate, []string, error) {
5261
}
5362
return certs, subjects, nil
5463
}
64+
65+
// BindingInspector receives as parameters a gRPC context and an Envelope,
66+
// and verifies whether the envelopes contains an appropriate binding to the context
67+
type BindingInspector func(context.Context, *common.Envelope) error
68+
69+
// NewBindingInspector returns a BindingInspector according to whether
70+
// mutualTLS is configured or not.
71+
func NewBindingInspector(mutualTLS bool) BindingInspector {
72+
if mutualTLS {
73+
return mutualTLSBinding
74+
}
75+
return noopBinding
76+
}
77+
78+
// mutualTLSBinding enforces the client to send its TLS cert hash in the
79+
// ChannelHeader, and then compares it to the computed hash that is derived
80+
// from the gRPC context.
81+
// In case they don't match, or the cert hash is missing from the request or
82+
// there is no TLS certificate to be excavated from the gRPC context,
83+
// an error is returned.
84+
func mutualTLSBinding(ctx context.Context, env *common.Envelope) error {
85+
if env == nil {
86+
return errors.New("envelope is nil")
87+
}
88+
ch, err := utils.ChannelHeader(env)
89+
if err != nil {
90+
return errors.Errorf("client didn't send a valid channel header: %v", err)
91+
}
92+
claimedTLScertHash := ch.TlsCertHash
93+
if len(claimedTLScertHash) == 0 {
94+
return errors.Errorf("client didn't include its TLS cert hash, error is: %v", err)
95+
}
96+
actualTLScertHash := ExtractCertificateHashFromContext(ctx)
97+
if len(actualTLScertHash) == 0 {
98+
return errors.Errorf("client didn't send a TLS certificate")
99+
}
100+
if !bytes.Equal(actualTLScertHash, claimedTLScertHash) {
101+
return errors.Errorf("claimed TLS cert hash is %v but actual TLS cert hash is %v", claimedTLScertHash, actualTLScertHash)
102+
}
103+
return nil
104+
}
105+
106+
// noopBinding is a BindingInspector that always returns nil
107+
func noopBinding(_ context.Context, _ *common.Envelope) error {
108+
return nil
109+
}
110+
111+
// ExtractCertificateHashFromContext extracts the hash of the certificate from the given context
112+
func ExtractCertificateHashFromContext(ctx context.Context) []byte {
113+
pr, extracted := peer.FromContext(ctx)
114+
if !extracted {
115+
return nil
116+
}
117+
118+
authInfo := pr.AuthInfo
119+
if authInfo == nil {
120+
return nil
121+
}
122+
123+
tlsInfo, isTLSConn := authInfo.(credentials.TLSInfo)
124+
if !isTLSConn {
125+
return nil
126+
}
127+
certs := tlsInfo.State.PeerCertificates
128+
if len(certs) == 0 {
129+
return nil
130+
}
131+
raw := certs[0].Raw
132+
if len(raw) == 0 {
133+
return nil
134+
}
135+
return util.ComputeSHA256(raw)
136+
}

core/comm/util_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package comm_test
8+
9+
import (
10+
"context"
11+
"crypto/tls"
12+
"crypto/x509"
13+
"sync/atomic"
14+
"testing"
15+
"time"
16+
17+
"github.com/golang/protobuf/proto"
18+
"github.com/hyperledger/fabric/common/util"
19+
"github.com/hyperledger/fabric/core/comm"
20+
grpc_testdata "github.com/hyperledger/fabric/core/comm/testdata/grpc"
21+
"github.com/hyperledger/fabric/protos/common"
22+
"github.com/hyperledger/fabric/protos/utils"
23+
"github.com/stretchr/testify/assert"
24+
context2 "golang.org/x/net/context"
25+
"google.golang.org/grpc"
26+
"google.golang.org/grpc/credentials"
27+
"google.golang.org/grpc/peer"
28+
)
29+
30+
func TestExtractCertificateHashFromContext(t *testing.T) {
31+
assert.Nil(t, comm.ExtractCertificateHashFromContext(context.Background()))
32+
33+
p := &peer.Peer{}
34+
ctx := peer.NewContext(context.Background(), p)
35+
assert.Nil(t, comm.ExtractCertificateHashFromContext(ctx))
36+
37+
p.AuthInfo = &nonTLSConnection{}
38+
ctx = peer.NewContext(context.Background(), p)
39+
assert.Nil(t, comm.ExtractCertificateHashFromContext(ctx))
40+
41+
p.AuthInfo = credentials.TLSInfo{}
42+
ctx = peer.NewContext(context.Background(), p)
43+
assert.Nil(t, comm.ExtractCertificateHashFromContext(ctx))
44+
45+
p.AuthInfo = credentials.TLSInfo{
46+
State: tls.ConnectionState{
47+
PeerCertificates: []*x509.Certificate{
48+
{},
49+
},
50+
},
51+
}
52+
ctx = peer.NewContext(context.Background(), p)
53+
assert.Nil(t, comm.ExtractCertificateHashFromContext(ctx))
54+
}
55+
56+
type nonTLSConnection struct {
57+
}
58+
59+
func (*nonTLSConnection) AuthType() string {
60+
return ""
61+
}
62+
63+
func TestNoopBindingInspector(t *testing.T) {
64+
// A Noop binding inspector always returns nil
65+
assert.Nil(t, comm.NewBindingInspector(false)(context.Background(), nil))
66+
}
67+
68+
func TestBindingInspector(t *testing.T) {
69+
testAddress := "localhost:25000"
70+
srv := newInspectingServer(testAddress, comm.NewBindingInspector(true))
71+
go srv.Start()
72+
defer srv.Stop()
73+
time.Sleep(time.Second)
74+
75+
// Scenario I: Invalid header sent
76+
err := srv.newInspection(t).inspectBinding(nil)
77+
assert.Error(t, err)
78+
assert.Contains(t, err.Error(), "envelope is nil")
79+
80+
// Scenario II: invalid channel header
81+
ch, _ := proto.Marshal(utils.MakeChannelHeader(common.HeaderType_CONFIG, 0, "test", 0))
82+
// Corrupt channel header
83+
ch = append(ch, 0)
84+
err = srv.newInspection(t).inspectBinding(envelopeWithChannelHeader(ch))
85+
assert.Error(t, err)
86+
assert.Contains(t, err.Error(), "client didn't send a valid channel header")
87+
88+
// Scenario III: No TLS cert hash in envelope
89+
chanHdr := utils.MakeChannelHeader(common.HeaderType_CONFIG, 0, "test", 0)
90+
ch, _ = proto.Marshal(chanHdr)
91+
err = srv.newInspection(t).inspectBinding(envelopeWithChannelHeader(ch))
92+
assert.Error(t, err)
93+
assert.Contains(t, err.Error(), "client didn't include its TLS cert hash")
94+
95+
// Scenario IV: Client sends its TLS cert hash as needed, but doesn't use mutual TLS
96+
cert, _ := tls.X509KeyPair([]byte(selfSignedCertPEM), []byte(selfSignedKeyPEM))
97+
chanHdr.TlsCertHash = util.ComputeSHA256([]byte(cert.Certificate[0]))
98+
ch, _ = proto.Marshal(chanHdr)
99+
err = srv.newInspection(t).inspectBinding(envelopeWithChannelHeader(ch))
100+
assert.Error(t, err)
101+
assert.Contains(t, err.Error(), "client didn't send a TLS certificate")
102+
103+
// Scenario V: Client uses mutual TLS but sends the wrong TLS cert hash
104+
chanHdr.TlsCertHash = []byte{1, 2, 3}
105+
chHdrWithWrongTLSCertHash, _ := proto.Marshal(chanHdr)
106+
err = srv.newInspection(t).withMutualTLS().inspectBinding(envelopeWithChannelHeader(chHdrWithWrongTLSCertHash))
107+
assert.Error(t, err)
108+
assert.Contains(t, err.Error(), "claimed TLS cert hash is [1 2 3] but actual TLS cert hash is")
109+
110+
// Scenario VI: Client uses mutual TLS and also sends the correct TLS cert hash
111+
err = srv.newInspection(t).withMutualTLS().inspectBinding(envelopeWithChannelHeader(ch))
112+
assert.NoError(t, err)
113+
}
114+
115+
type inspectingServer struct {
116+
addr string
117+
comm.GRPCServer
118+
lastContext atomic.Value
119+
inspector comm.BindingInspector
120+
}
121+
122+
func (is *inspectingServer) EmptyCall(ctx context2.Context, _ *grpc_testdata.Empty) (*grpc_testdata.Empty, error) {
123+
is.lastContext.Store(ctx)
124+
return &grpc_testdata.Empty{}, nil
125+
}
126+
127+
func (is *inspectingServer) inspect(envelope *common.Envelope) error {
128+
return is.inspector(is.lastContext.Load().(context2.Context), envelope)
129+
}
130+
131+
func newInspectingServer(addr string, inspector comm.BindingInspector) *inspectingServer {
132+
srv, err := comm.NewGRPCServer(addr, comm.SecureServerConfig{
133+
UseTLS: true,
134+
ServerCertificate: []byte(selfSignedCertPEM),
135+
ServerKey: []byte(selfSignedKeyPEM),
136+
})
137+
if err != nil {
138+
panic(err)
139+
}
140+
is := &inspectingServer{
141+
addr: addr,
142+
GRPCServer: srv,
143+
inspector: inspector,
144+
}
145+
grpc_testdata.RegisterTestServiceServer(srv.Server(), is)
146+
return is
147+
}
148+
149+
type inspection struct {
150+
tlsConfig *tls.Config
151+
server *inspectingServer
152+
creds credentials.TransportCredentials
153+
t *testing.T
154+
}
155+
156+
func (is *inspectingServer) newInspection(t *testing.T) *inspection {
157+
tlsConfig := &tls.Config{
158+
RootCAs: x509.NewCertPool(),
159+
}
160+
tlsConfig.RootCAs.AppendCertsFromPEM([]byte(selfSignedCertPEM))
161+
return &inspection{
162+
server: is,
163+
creds: credentials.NewTLS(tlsConfig),
164+
t: t,
165+
tlsConfig: tlsConfig,
166+
}
167+
}
168+
169+
func (ins *inspection) withMutualTLS() *inspection {
170+
cert, err := tls.X509KeyPair([]byte(selfSignedCertPEM), []byte(selfSignedKeyPEM))
171+
assert.NoError(ins.t, err)
172+
ins.tlsConfig.Certificates = []tls.Certificate{cert}
173+
ins.creds = credentials.NewTLS(ins.tlsConfig)
174+
return ins
175+
}
176+
177+
func (ins *inspection) inspectBinding(envelope *common.Envelope) error {
178+
ctx := context.Background()
179+
ctx, c := context.WithTimeout(ctx, time.Second*3)
180+
defer c()
181+
conn, err := grpc.DialContext(ctx, ins.server.addr, grpc.WithTransportCredentials(ins.creds), grpc.WithBlock())
182+
defer conn.Close()
183+
assert.NoError(ins.t, err)
184+
_, err = grpc_testdata.NewTestServiceClient(conn).EmptyCall(context.Background(), &grpc_testdata.Empty{})
185+
return ins.server.inspect(envelope)
186+
}
187+
188+
func envelopeWithChannelHeader(ch []byte) *common.Envelope {
189+
pl := &common.Payload{
190+
Header: &common.Header{
191+
ChannelHeader: ch,
192+
},
193+
}
194+
payload, _ := proto.Marshal(pl)
195+
return &common.Envelope{
196+
Payload: payload,
197+
}
198+
}

protos/common/collection.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)