-
Notifications
You must be signed in to change notification settings - Fork 0
/
tlscfg.go
351 lines (311 loc) · 10.3 KB
/
tlscfg.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
// Package tlscfg provides an option-driven way to easily setup a well
// configured *tls.Config.
//
// Initializing a *tls.Config is a rote task, and often good, secure defaults
// are not so obvious. This package aims to eliminate the chore of initializing
// a *tls.Config correctly and securely.
//
// New returns a valid config with system certificates and tls v1.2+ ciphers.
// The With functions can be used to further add certificates or override
// settings as appropriate.
//
// Usage:
//
// cfg, err := tlscfg.New(
// tlscfg.MaybeWithDiskCA( // optional CA
// *flagCA,
// tlscfg.ForClient,
// ),
// tlscfg.WithDiskKeyPair( // required client cert+key pair
// "cert.pem",
// "key.pem",
// ),
// )
// if err != nil {
// // handle
// }
package tlscfg
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
)
// Opt is a function to configure a *tls.Config.
type Opt interface {
apply(FS, *tls.Config) error
}
type opt struct {
fn func(FS, *tls.Config) error
}
func (o *opt) apply(fs FS, c *tls.Config) error { return o.fn(fs, c) }
// ForKind is used in some options to specify whether the option is meant to be
// applied for server configurations or client configurations.
type ForKind uint8
const (
// ForServer specifies that an option should work on server portions
// of a *tls.Config (adding a CA).
ForServer ForKind = iota
// ForClient specifies that an option should work on client portions
// of a *tls.Config (adding a CA).
ForClient
)
// CipherSuites returns this package's recommended ciphers that tls
// configurations should use.
//
// Currently, this returns tls ciphers that are only compatible with tls v1.2.
// Ciphers for tls v1.3 are not include because if a connection negotiates tls
// v1.3, Go internally uses v1.3 ciphers.
func CipherSuites() []uint16 {
return []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
}
}
// CurvePreferences returns this package's recommended curve preferences that
// tls configurations should use.
//
// Currently, this returns only x25519. This may cause problems with old
// versions of openssl, if so, be sure to add P256.
//
// This package by default does not set CurvePreferences, relying on the Go
// default. However, you can opt into this package's CurvePreferences for
// added paranoia.
func CurvePreferences() []tls.CurveID {
return []tls.CurveID{tls.X25519}
}
// MaybeWithDiskKeyPair optionally loads a PEM encoded cert and key from
// certPath and keyPath and adds the pair to the *tls.Config's Certificates.
//
// If both certPath and keyPath are empty, this option does nothing. This
// option is useful if accepting flags to optionally setup a cert.
func MaybeWithDiskKeyPair(certPath, keyPath string) Opt {
return &opt{func(fs FS, cfg *tls.Config) error {
if certPath == "" && keyPath == "" {
return nil
}
return WithDiskKeyPair(certPath, keyPath).apply(fs, cfg)
}}
}
// WithDiskKeyPair loads a PEM encoded cert and key from certPath and keyPath
// and adds the pair to the *tls.Config's Certificates.
func WithDiskKeyPair(certPath, keyPath string) Opt {
return &opt{func(fs FS, cfg *tls.Config) error {
if certPath == "" || keyPath == "" {
return errors.New("both cert and key paths must be specified")
}
cert, err := fs.ReadFile(certPath)
if err != nil {
return fmt.Errorf("unable to read cert at %q: %w", certPath, err)
}
pem, err := fs.ReadFile(keyPath)
if err != nil {
return fmt.Errorf("unable to read key at %q: %w", keyPath, err)
}
return WithKeyPair(cert, pem).apply(fs, cfg)
}}
}
// WithKeyPair parses a PEM encoded cert and key and adds the pair to the
// *tls.Config's Certificates.
func WithKeyPair(cert, key []byte) Opt {
return &opt{func(_ FS, cfg *tls.Config) error {
cert, err := tls.X509KeyPair(cert, key)
if err != nil {
return fmt.Errorf("unable to load keypair: %w", err)
}
cfg.Certificates = append(cfg.Certificates, cert)
return nil
}}
}
// MaybeWithDiskCA optionally loads a PEM encoded CA cert from disk and adds it
// to the proper CA pool based off of forKind.
//
// If the path is empty, this option does nothing. This option is useful if
// accepting flags to optionally setup a cert.
//
// NOTE: If this option loads a CA, then system certs are not used. If you wish
// to use system certs in addition to this CA, use the WithSystemCertPool option.
func MaybeWithDiskCA(path string, forKind ForKind) Opt {
return &opt{func(fs FS, cfg *tls.Config) error {
if path == "" {
return nil
}
return WithDiskCA(path, forKind).apply(fs, cfg)
}}
}
// WithDiskCA loads a PEM encoded CA cert from disk and adds it to the proper
// CA pool based off of forKind.
//
// If for servers, this option sets RequireAndVerifyClientCert.
//
// NOTE: This option ensures system certs are not used. If you wish to use
// system certs in addition to this CA, use the WithSystemCertPool option.
func WithDiskCA(path string, forKind ForKind) Opt {
return &opt{func(fs FS, cfg *tls.Config) error {
if path == "" {
return errors.New("ca path must be specified")
}
ca, err := fs.ReadFile(path)
if err != nil {
return fmt.Errorf("unable to read ca at %q: %w", path, err)
}
return WithCA(ca, forKind).apply(fs, cfg)
}}
}
// WithCA parses a PEM encoded CA cert and adds it to the proper CA pool based
// off of forKind.
//
// If for servers, this option sets RequireAndVerifyClientCert.
//
// NOTE: This option ensures system certs are not used. If you wish to use
// system certs in addition to this CA, use the WithSystemCertPool option.
func WithCA(ca []byte, forKind ForKind) Opt {
return &opt{func(_ FS, cfg *tls.Config) error {
pool := &cfg.RootCAs
if forKind == ForServer {
cfg.ClientAuth = tls.RequireAndVerifyClientCert
pool = &cfg.ClientCAs
}
// If the special systemCASentinel is set, then system certs
// were requested in addition to the custom CA. We initialize
// the pool with system certs and add.
switch *pool {
case nil:
*pool = x509.NewCertPool()
case systemCASentinel:
var err error
if *pool, err = x509.SystemCertPool(); err != nil {
return fmt.Errorf("unable to load system cert pool: %w", err)
}
}
if ok := (*pool).AppendCertsFromPEM(ca); !ok {
return errors.New("no cert could be found in the ca bytes")
}
return nil
}}
}
var (
systemCA = &opt{func(FS, *tls.Config) error { return nil }}
systemCASentinel = new(x509.CertPool)
)
// WithSystemCertPool ensures that the system cert pool is used in addition to
// any CA you manually set.
//
// This option is necessary if you want to talk to both servers that have
// public CA issued certs as well as servers that have your own manually issued
// certs, or, as a server, if you want to verify certs from clients with public
// CA issued certs as well as clients that use custom certs.
//
// This option is likely only to be used when migrating from mTLS custom certs
// to public CA certs.
//
// Only cert pools that have additional CAs to add are initialized. If no extra
// CAs are added, the pool is left nil, which by default uses system certs.
func WithSystemCertPool() Opt {
return systemCA
}
// WithServerName sets the *tls.Config's ServerName, which is important for
// clients to verify servers. This option is required if all of the following
// are true:
//
// - the config is not used in an http.Transport (http.Transport clones the
// config and sets ServerName)
// - you do not set InsecureSkipVerify to true
// - you do not want to set ServerName on the config manually
func WithServerName(name string) Opt {
return &opt{func(_ FS, cfg *tls.Config) error {
cfg.ServerName = name
return nil
}}
}
// WithAdditionalCipherSuites adds additional cipher suites to the default set
// used by this package. This option is important if talking to legacy systems
// that do not support newer cipher suites.
func WithAdditionalCipherSuites(cipherSuites ...uint16) Opt {
return &opt{func(_ FS, cfg *tls.Config) error {
cfg.CipherSuites = append(cfg.CipherSuites, cipherSuites...)
return nil
}}
}
type override struct {
fn func(*tls.Config) error
}
func (o *override) apply(_ FS, c *tls.Config) error { return o.fn(c) }
// WithOverride returns an option to override fields on a *tls.Config. All
// overrides are run last, in order.
func WithOverride(fn func(*tls.Config) error) Opt {
return &override{fn}
}
type filesystem struct{ fs FS }
func (*filesystem) apply(FS, *tls.Config) error { panic("unused") }
// WithFS sets the filesystem used to read files, overriding the default of
// simply using the host OS.
func WithFS(fs FS) Opt {
return &filesystem{fs}
}
// FS represents a filesystem.
//
// This is different from fs.FS, because fs.FS only reads unrooted paths.
type FS interface {
// ReadFile opens the file at path and reads it.
ReadFile(path string) ([]byte, error)
}
type funcFS struct {
fn func(string) ([]byte, error)
}
func (f *funcFS) ReadFile(path string) ([]byte, error) { return f.fn(path) }
// FuncFS returns a FS that reads a file using the given function.
func FuncFS(fn func(path string) ([]byte, error)) FS {
return &funcFS{fn}
}
var osFS FS = FuncFS(func(path string) ([]byte, error) { return os.ReadFile(path) })
// New creates and returns a *tls.Config with any options applied.
//
// This function will not error if no options are specified.
func New(opts ...Opt) (*tls.Config, error) {
cfg := &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: CipherSuites(),
}
var (
fs = osFS
first []Opt
last []Opt
)
for _, o := range opts {
switch t := o.(type) {
case *filesystem:
fs = t.fs
case *opt:
if t == systemCA {
cfg.ClientCAs = systemCASentinel
cfg.RootCAs = systemCASentinel
continue
}
first = append(first, t)
case *override:
last = append(last, t)
}
}
// Before we apply overrides, strip our sentinel pointer.
first = append(first, &opt{func(_ FS, c *tls.Config) error {
if c.ClientCAs == systemCASentinel {
c.ClientCAs = nil
}
if c.RootCAs == systemCASentinel {
c.RootCAs = nil
}
return nil
}})
for _, opt := range append(first, last...) {
if err := opt.apply(fs, cfg); err != nil {
return nil, err
}
}
return cfg, nil
}