forked from hashicorp/consul
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tls.go
441 lines (387 loc) · 14.1 KB
/
tls.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package connect
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/url"
"os"
"strings"
"sync"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/api"
)
// parseLeafX509Cert will parse an X509 certificate
// from the TLS certificate and store the parsed
// value in the TLS certificate as the Leaf field.
func parseLeafX509Cert(leaf *tls.Certificate) error {
if leaf == nil {
// nothing to parse for nil cert
return nil
}
if leaf.Leaf != nil {
// leaf cert was already parsed
return nil
}
cert, err := x509.ParseCertificate(leaf.Certificate[0])
if err != nil {
return err
}
leaf.Leaf = cert
return nil
}
// verifierFunc is a function that can accept rawCertificate bytes from a peer
// and verify them against a given tls.Config. It's called from the
// tls.Config.VerifyPeerCertificate hook.
//
// We don't pass verifiedChains since that is always nil in our usage.
// Implementations can use the roots provided in the cfg to verify the certs.
//
// The passed *tls.Config may have a nil VerifyPeerCertificates function but
// will have correct roots, leaf and other fields.
type verifierFunc func(cfg *tls.Config, rawCerts [][]byte) error
// defaultTLSConfig returns the standard config with no peer verifier. It is
// insecure to use it as-is.
func defaultTLSConfig() *tls.Config {
cfg := &tls.Config{
MinVersion: tls.VersionTLS12,
ClientAuth: tls.RequireAndVerifyClientCert,
// We don't have access to go internals that decide if AES hardware
// acceleration is available in order to prefer CHA CHA if not. So let's
// just always prefer AES for now. We can look into doing something uglier
// later like using an external lib for AES checking if it seems important.
// https://github.com/golang/go/blob/df91b8044dbe790c69c16058330f545be069cc1f/src/crypto/tls/common.go#L919:14
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
},
// We have to set this since otherwise Go will attempt to verify DNS names
// match DNS SAN/CN which we don't want. We hook up VerifyPeerCertificate to
// do our own path validation as well as Connect AuthZ.
InsecureSkipVerify: true,
// Include h2 to allow connect http servers to automatically support http2.
// See: https://github.com/golang/go/blob/917c33fe8672116b04848cf11545296789cafd3b/src/net/http/server.go#L2724-L2731
NextProtos: []string{"h2"},
}
return cfg
}
// devTLSConfigFromFiles returns a default TLS Config but with certs and CAs
// based on local files for dev. No verification is setup.
func devTLSConfigFromFiles(caFile, certFile,
keyFile string) (*tls.Config, error) {
roots := x509.NewCertPool()
bs, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}
roots.AppendCertsFromPEM(bs)
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
cfg := defaultTLSConfig()
cfg.Certificates = []tls.Certificate{cert}
cfg.RootCAs = roots
cfg.ClientCAs = roots
return cfg, nil
}
// CertURIFromConn is a helper to extract the service identifier URI from a
// net.Conn. If the net.Conn is not a *tls.Conn then an error is always
// returned. If the *tls.Conn didn't present a valid connect certificate, or is
// not yet past the handshake, an error is returned.
func CertURIFromConn(conn net.Conn) (connect.CertURI, error) {
tc, ok := conn.(*tls.Conn)
if !ok {
return nil, fmt.Errorf("invalid non-TLS connect client")
}
gotURI, err := extractCertURI(tc.ConnectionState().PeerCertificates)
if err != nil {
return nil, err
}
return connect.ParseCertURI(gotURI)
}
// extractCertURI returns the first URI SAN from the leaf certificate presented
// in the slice. The slice is expected to be the passed from
// tls.Conn.ConnectionState().PeerCertificates and requires that the leaf has at
// least one URI and the first URI is the correct one to use.
func extractCertURI(certs []*x509.Certificate) (*url.URL, error) {
if len(certs) < 1 {
return nil, errors.New("no peer certificate presented")
}
// Only check the first cert assuming this is the only leaf. It's not clear if
// services might ever legitimately present multiple leaf certificates or if
// the slice is just to allow presenting the whole chain of intermediates.
cert := certs[0]
// Our certs will only ever have a single URI for now so only check that
if len(cert.URIs) < 1 {
return nil, errors.New("peer certificate invalid")
}
return cert.URIs[0], nil
}
// verifyServerCertMatchesURI is used on tls connections dialed to a connect
// server to ensure that the certificate it presented has the correct identity.
func verifyServerCertMatchesURI(certs []*x509.Certificate, expected connect.CertURI) error {
expectedStr := expected.URI().String()
gotURI, err := extractCertURI(certs)
if err != nil {
return errors.New("peer certificate mismatch")
}
// Override the hostname since we rely on x509 constraints to limit ability to
// spoof the trust domain if needed (i.e. because a root is shared with other
// PKI or Consul clusters). This allows for seamless migrations between trust
// domains.
expectURI := expected.URI()
expectURI.Host = gotURI.Host
if strings.EqualFold(gotURI.String(), expectURI.String()) {
return nil
}
return fmt.Errorf("peer certificate mismatch got %s, want %s",
gotURI.String(), expectedStr)
}
// newServerSideVerifier returns a verifierFunc that wraps the provided
// api.Client to verify the TLS chain and perform AuthZ for the server end of
// the connection. The service name provided is used as the target service name
// for the Authorization.
func newServerSideVerifier(logger hclog.Logger, client *api.Client, serviceName string) verifierFunc {
return func(tlsCfg *tls.Config, rawCerts [][]byte) error {
leaf, err := verifyChain(tlsCfg, rawCerts, false)
if err != nil {
logger.Error("failed TLS verification", "error", err)
return err
}
// Check leaf is a cert we understand
if len(leaf.URIs) < 1 {
logger.Error("invalid leaf certificate: no URIs set")
return errors.New("connect: invalid leaf certificate")
}
certURI, err := connect.ParseCertURI(leaf.URIs[0])
if err != nil {
logger.Error("invalid leaf certificate URI", "error", err)
return errors.New("connect: invalid leaf certificate URI")
}
// No AuthZ if there is no client.
if client == nil {
logger.Info("nil client provided")
return nil
}
// Perform AuthZ
req := &api.AgentAuthorizeParams{
Target: serviceName,
ClientCertURI: certURI.URI().String(),
ClientCertSerial: connect.EncodeSerialNumber(leaf.SerialNumber),
}
resp, err := client.Agent().ConnectAuthorize(req)
if err != nil {
logger.Error("authz call failed", "error", err)
return errors.New("connect: authz call failed: " + err.Error())
}
if !resp.Authorized {
logger.Error("authz call denied", "reason", resp.Reason)
return errors.New("connect: authz denied: " + resp.Reason)
}
return nil
}
}
// clientSideVerifier is a verifierFunc that performs verification of certificates
// on the client end of the connection. For now it is just basic TLS
// verification since the identity check needs additional state and becomes
// clunky to customize the callback for every outgoing request. That is done
// within Service.Dial for now.
func clientSideVerifier(tlsCfg *tls.Config, rawCerts [][]byte) error {
_, err := verifyChain(tlsCfg, rawCerts, true)
return err
}
// verifyChain performs standard TLS verification without enforcing remote
// hostname matching.
func verifyChain(tlsCfg *tls.Config, rawCerts [][]byte, client bool) (*x509.Certificate, error) {
// Fetch leaf and intermediates. This is based on code form tls handshake.
if len(rawCerts) < 1 {
return nil, errors.New("tls: no certificates from peer")
}
certs := make([]*x509.Certificate, len(rawCerts))
for i, asn1Data := range rawCerts {
cert, err := x509.ParseCertificate(asn1Data)
if err != nil {
return nil, errors.New("tls: failed to parse certificate from peer: " + err.Error())
}
certs[i] = cert
}
cas := tlsCfg.RootCAs
if client {
cas = tlsCfg.ClientCAs
}
opts := x509.VerifyOptions{
Roots: cas,
Intermediates: x509.NewCertPool(),
}
if !client {
// Server side only sets KeyUsages in tls. This defaults to ServerAuth in
// x509 lib. See
// https://github.com/golang/go/blob/ee7dd810f9ca4e63ecfc1d3044869591783b8b74/src/crypto/x509/verify.go#L866-L868
opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
}
// All but the first cert are intermediates
for _, cert := range certs[1:] {
opts.Intermediates.AddCert(cert)
}
_, err := certs[0].Verify(opts)
return certs[0], err
}
// dynamicTLSConfig represents the state for returning a tls.Config that can
// have root and leaf certificates updated dynamically with all existing clients
// and servers automatically picking up the changes. It requires initializing
// with a valid base config from which all the non-certificate and verification
// params are used. The base config passed should not be modified externally as
// it is assumed to be serialized by the embedded mutex.
type dynamicTLSConfig struct {
base *tls.Config
sync.RWMutex
leaf *tls.Certificate
roots *x509.CertPool
// readyCh is closed when the config first gets both leaf and roots set.
// Watchers can wait on this via ReadyWait.
readyCh chan struct{}
}
type tlsCfgUpdate struct {
ch chan struct{}
next *tlsCfgUpdate
}
// newDynamicTLSConfig returns a dynamicTLSConfig constructed from base.
// base.Certificates[0] is used as the initial leaf and base.RootCAs is used as
// the initial roots.
func newDynamicTLSConfig(base *tls.Config, logger hclog.Logger) *dynamicTLSConfig {
cfg := &dynamicTLSConfig{
base: base,
}
if len(base.Certificates) > 0 {
cfg.leaf = &base.Certificates[0]
// If this does error then future calls to Ready will fail
// It is better to handle not-Ready rather than failing
if err := parseLeafX509Cert(cfg.leaf); err != nil && logger != nil {
logger.Error("error parsing configured leaf certificate", "error", err)
}
}
if base.RootCAs != nil {
cfg.roots = base.RootCAs
}
if !cfg.Ready() {
cfg.readyCh = make(chan struct{})
}
return cfg
}
// Get fetches the lastest tls.Config with all the hooks attached to keep it
// loading the most recent roots and certs even after future changes to cfg.
//
// The verifierFunc passed will be attached to the config returned such that it
// runs with the _latest_ config object returned passed to it. That means that a
// client can use this config for a long time and will still verify against the
// latest roots even though the roots in the struct is has can't change.
func (cfg *dynamicTLSConfig) Get(v verifierFunc) *tls.Config {
cfg.RLock()
defer cfg.RUnlock()
copy := cfg.base.Clone()
copy.RootCAs = cfg.roots
copy.ClientCAs = cfg.roots
if v != nil {
copy.VerifyPeerCertificate = func(rawCerts [][]byte, chains [][]*x509.Certificate) error {
return v(cfg.Get(nil), rawCerts)
}
}
copy.GetCertificate = func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
leaf := cfg.Leaf()
if leaf == nil {
return nil, errors.New("tls: no certificates configured")
}
return leaf, nil
}
copy.GetClientCertificate = func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
leaf := cfg.Leaf()
if leaf == nil {
return nil, errors.New("tls: no certificates configured")
}
return leaf, nil
}
copy.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
return cfg.Get(v), nil
}
return copy
}
// SetRoots sets new roots.
func (cfg *dynamicTLSConfig) SetRoots(roots *x509.CertPool) error {
cfg.Lock()
defer cfg.Unlock()
cfg.roots = roots
cfg.notify()
return nil
}
// SetLeaf sets a new leaf.
func (cfg *dynamicTLSConfig) SetLeaf(leaf *tls.Certificate) error {
cfg.Lock()
defer cfg.Unlock()
if err := parseLeafX509Cert(leaf); err != nil {
return err
}
cfg.leaf = leaf
cfg.notify()
return nil
}
// notify is called under lock during an update to check if we are now ready.
func (cfg *dynamicTLSConfig) notify() {
if cfg.readyCh != nil && cfg.leaf != nil && cfg.roots != nil && cfg.leaf.Leaf != nil {
close(cfg.readyCh)
cfg.readyCh = nil
}
}
func (cfg *dynamicTLSConfig) VerifyLeafWithRoots() error {
cfg.RLock()
defer cfg.RUnlock()
if cfg.roots == nil {
return fmt.Errorf("No roots are set")
} else if cfg.leaf == nil {
return fmt.Errorf("No leaf certificate is set")
} else if cfg.leaf.Leaf == nil {
return fmt.Errorf("Leaf certificate has not been parsed")
}
_, err := cfg.leaf.Leaf.Verify(x509.VerifyOptions{Roots: cfg.roots})
return err
}
// Roots returns the current CA root CertPool.
func (cfg *dynamicTLSConfig) Roots() *x509.CertPool {
cfg.RLock()
defer cfg.RUnlock()
return cfg.roots
}
// Leaf returns the current Leaf certificate.
func (cfg *dynamicTLSConfig) Leaf() *tls.Certificate {
cfg.RLock()
defer cfg.RUnlock()
return cfg.leaf
}
// Ready returns whether or not both roots and a leaf certificate are
// configured. If both are non-nil, they are assumed to be valid and usable.
func (cfg *dynamicTLSConfig) Ready() bool {
// not locking because VerifyLeafWithRoots will do that
return cfg.VerifyLeafWithRoots() == nil
}
// ReadyWait returns a chan that is closed when the the Service becomes ready
// for use for the first time. Note that if the Service is ready when it is
// called it returns a nil chan. Ready means that it has root and leaf
// certificates configured but not that the combination is valid nor that
// the current time is within the validity window of the certificate. The
// service may subsequently stop being "ready" if it's certificates expire
// or are revoked and an error prevents new ones from being loaded but this
// method will not stop returning a nil chan in that case. It is only useful
// for initial startup. For ongoing health Ready() should be used.
func (cfg *dynamicTLSConfig) ReadyWait() <-chan struct{} {
cfg.RLock()
defer cfg.RUnlock()
return cfg.readyCh
}