blob: 1b31bbb8b1b96c9929b4e9d69408cf3d9bf67469 [file] [log] [blame]
David K. Bainbridge215e0242017-09-05 23:18:24 -07001// Package tlsconfig provides primitives to retrieve secure-enough TLS configurations for both clients and servers.
2//
3// As a reminder from https://golang.org/pkg/crypto/tls/#Config:
4// A Config structure is used to configure a TLS client or server. After one has been passed to a TLS function it must not be modified.
5// A Config may be reused; the tls package will also not modify it.
6package tlsconfig
7
8import (
9 "crypto/tls"
10 "crypto/x509"
11 "encoding/pem"
12 "fmt"
13 "io/ioutil"
14 "os"
15
16 "github.com/pkg/errors"
17)
18
19// Options represents the information needed to create client and server TLS configurations.
20type Options struct {
21 CAFile string
22
23 // If either CertFile or KeyFile is empty, Client() will not load them
24 // preventing the client from authenticating to the server.
25 // However, Server() requires them and will error out if they are empty.
26 CertFile string
27 KeyFile string
28
29 // client-only option
30 InsecureSkipVerify bool
31 // server-only option
32 ClientAuth tls.ClientAuthType
33 // If ExclusiveRootPools is set, then if a CA file is provided, the root pool used for TLS
34 // creds will include exclusively the roots in that CA file. If no CA file is provided,
35 // the system pool will be used.
36 ExclusiveRootPools bool
37 MinVersion uint16
38 // If Passphrase is set, it will be used to decrypt a TLS private key
39 // if the key is encrypted
40 Passphrase string
41}
42
43// Extra (server-side) accepted CBC cipher suites - will phase out in the future
44var acceptedCBCCiphers = []uint16{
45 tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
46 tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
47 tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
48 tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
49 tls.TLS_RSA_WITH_AES_256_CBC_SHA,
50 tls.TLS_RSA_WITH_AES_128_CBC_SHA,
51}
52
53// DefaultServerAcceptedCiphers should be uses by code which already has a crypto/tls
54// options struct but wants to use a commonly accepted set of TLS cipher suites, with
55// known weak algorithms removed.
56var DefaultServerAcceptedCiphers = append(clientCipherSuites, acceptedCBCCiphers...)
57
58// allTLSVersions lists all the TLS versions and is used by the code that validates
59// a uint16 value as a TLS version.
60var allTLSVersions = map[uint16]struct{}{
61 tls.VersionSSL30: {},
62 tls.VersionTLS10: {},
63 tls.VersionTLS11: {},
64 tls.VersionTLS12: {},
65}
66
67// ServerDefault returns a secure-enough TLS configuration for the server TLS configuration.
68func ServerDefault() *tls.Config {
69 return &tls.Config{
70 // Avoid fallback to SSL protocols < TLS1.0
71 MinVersion: tls.VersionTLS10,
72 PreferServerCipherSuites: true,
73 CipherSuites: DefaultServerAcceptedCiphers,
74 }
75}
76
77// ClientDefault returns a secure-enough TLS configuration for the client TLS configuration.
78func ClientDefault() *tls.Config {
79 return &tls.Config{
80 // Prefer TLS1.2 as the client minimum
81 MinVersion: tls.VersionTLS12,
82 CipherSuites: clientCipherSuites,
83 }
84}
85
86// certPool returns an X.509 certificate pool from `caFile`, the certificate file.
87func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) {
88 // If we should verify the server, we need to load a trusted ca
89 var (
90 certPool *x509.CertPool
91 err error
92 )
93 if exclusivePool {
94 certPool = x509.NewCertPool()
95 } else {
96 certPool, err = SystemCertPool()
97 if err != nil {
98 return nil, fmt.Errorf("failed to read system certificates: %v", err)
99 }
100 }
101 pem, err := ioutil.ReadFile(caFile)
102 if err != nil {
103 return nil, fmt.Errorf("could not read CA certificate %q: %v", caFile, err)
104 }
105 if !certPool.AppendCertsFromPEM(pem) {
106 return nil, fmt.Errorf("failed to append certificates from PEM file: %q", caFile)
107 }
108 return certPool, nil
109}
110
111// isValidMinVersion checks that the input value is a valid tls minimum version
112func isValidMinVersion(version uint16) bool {
113 _, ok := allTLSVersions[version]
114 return ok
115}
116
117// adjustMinVersion sets the MinVersion on `config`, the input configuration.
118// It assumes the current MinVersion on the `config` is the lowest allowed.
119func adjustMinVersion(options Options, config *tls.Config) error {
120 if options.MinVersion > 0 {
121 if !isValidMinVersion(options.MinVersion) {
122 return fmt.Errorf("Invalid minimum TLS version: %x", options.MinVersion)
123 }
124 if options.MinVersion < config.MinVersion {
125 return fmt.Errorf("Requested minimum TLS version is too low. Should be at-least: %x", config.MinVersion)
126 }
127 config.MinVersion = options.MinVersion
128 }
129
130 return nil
131}
132
133// IsErrEncryptedKey returns true if the 'err' is an error of incorrect
134// password when tryin to decrypt a TLS private key
135func IsErrEncryptedKey(err error) bool {
136 return errors.Cause(err) == x509.IncorrectPasswordError
137}
138
139// getPrivateKey returns the private key in 'keyBytes', in PEM-encoded format.
140// If the private key is encrypted, 'passphrase' is used to decrypted the
141// private key.
142func getPrivateKey(keyBytes []byte, passphrase string) ([]byte, error) {
143 // this section makes some small changes to code from notary/tuf/utils/x509.go
144 pemBlock, _ := pem.Decode(keyBytes)
145 if pemBlock == nil {
146 return nil, fmt.Errorf("no valid private key found")
147 }
148
149 var err error
150 if x509.IsEncryptedPEMBlock(pemBlock) {
151 keyBytes, err = x509.DecryptPEMBlock(pemBlock, []byte(passphrase))
152 if err != nil {
153 return nil, errors.Wrap(err, "private key is encrypted, but could not decrypt it")
154 }
155 keyBytes = pem.EncodeToMemory(&pem.Block{Type: pemBlock.Type, Bytes: keyBytes})
156 }
157
158 return keyBytes, nil
159}
160
161// getCert returns a Certificate from the CertFile and KeyFile in 'options',
162// if the key is encrypted, the Passphrase in 'options' will be used to
163// decrypt it.
164func getCert(options Options) ([]tls.Certificate, error) {
165 if options.CertFile == "" && options.KeyFile == "" {
166 return nil, nil
167 }
168
169 errMessage := "Could not load X509 key pair"
170
171 cert, err := ioutil.ReadFile(options.CertFile)
172 if err != nil {
173 return nil, errors.Wrap(err, errMessage)
174 }
175
176 prKeyBytes, err := ioutil.ReadFile(options.KeyFile)
177 if err != nil {
178 return nil, errors.Wrap(err, errMessage)
179 }
180
181 prKeyBytes, err = getPrivateKey(prKeyBytes, options.Passphrase)
182 if err != nil {
183 return nil, errors.Wrap(err, errMessage)
184 }
185
186 tlsCert, err := tls.X509KeyPair(cert, prKeyBytes)
187 if err != nil {
188 return nil, errors.Wrap(err, errMessage)
189 }
190
191 return []tls.Certificate{tlsCert}, nil
192}
193
194// Client returns a TLS configuration meant to be used by a client.
195func Client(options Options) (*tls.Config, error) {
196 tlsConfig := ClientDefault()
197 tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify
198 if !options.InsecureSkipVerify && options.CAFile != "" {
199 CAs, err := certPool(options.CAFile, options.ExclusiveRootPools)
200 if err != nil {
201 return nil, err
202 }
203 tlsConfig.RootCAs = CAs
204 }
205
206 tlsCerts, err := getCert(options)
207 if err != nil {
208 return nil, err
209 }
210 tlsConfig.Certificates = tlsCerts
211
212 if err := adjustMinVersion(options, tlsConfig); err != nil {
213 return nil, err
214 }
215
216 return tlsConfig, nil
217}
218
219// Server returns a TLS configuration meant to be used by a server.
220func Server(options Options) (*tls.Config, error) {
221 tlsConfig := ServerDefault()
222 tlsConfig.ClientAuth = options.ClientAuth
223 tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile)
224 if err != nil {
225 if os.IsNotExist(err) {
226 return nil, fmt.Errorf("Could not load X509 key pair (cert: %q, key: %q): %v", options.CertFile, options.KeyFile, err)
227 }
228 return nil, fmt.Errorf("Error reading X509 key pair (cert: %q, key: %q): %v. Make sure the key is not encrypted.", options.CertFile, options.KeyFile, err)
229 }
230 tlsConfig.Certificates = []tls.Certificate{tlsCert}
231 if options.ClientAuth >= tls.VerifyClientCertIfGiven && options.CAFile != "" {
232 CAs, err := certPool(options.CAFile, options.ExclusiveRootPools)
233 if err != nil {
234 return nil, err
235 }
236 tlsConfig.ClientCAs = CAs
237 }
238
239 if err := adjustMinVersion(options, tlsConfig); err != nil {
240 return nil, err
241 }
242
243 return tlsConfig, nil
244}