Skip to content

Commit 9a08a30

Browse files
committed
add the ability to accept a passphrase and decrypt a TLS private key
Signed-off-by: Arash Deshmeh <[email protected]>
1 parent a2afab9 commit 9a08a30

File tree

4 files changed

+165
-9
lines changed

4 files changed

+165
-9
lines changed

tlsconfig/config.go

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ package tlsconfig
88
import (
99
"crypto/tls"
1010
"crypto/x509"
11+
"encoding/pem"
1112
"fmt"
1213
"io/ioutil"
1314
"os"
1415

1516
"github.com/Sirupsen/logrus"
17+
"github.com/pkg/errors"
1618
)
1719

1820
// Options represents the information needed to create client and server TLS configurations.
@@ -34,6 +36,9 @@ type Options struct {
3436
// the system pool will be used.
3537
ExclusiveRootPools bool
3638
MinVersion uint16
39+
// If Passphrase is set, it will be used to decrypt a TLS private key
40+
// if the key is encrypted
41+
Passphrase string
3742
}
3843

3944
// Extra (server-side) accepted CBC cipher suites - will phase out in the future
@@ -127,6 +132,67 @@ func adjustMinVersion(options Options, config *tls.Config) error {
127132
return nil
128133
}
129134

135+
// IsErrEncryptedKey returns true if the 'err' is an error of incorrect
136+
// password when tryin to decrypt a TLS private key
137+
func IsErrEncryptedKey(err error) bool {
138+
return errors.Cause(err) == x509.IncorrectPasswordError
139+
}
140+
141+
// getPrivateKey returns the private key in 'keyBytes', in PEM-encoded format.
142+
// If the private key is encrypted, 'passphrase' is used to decrypted the
143+
// private key.
144+
func getPrivateKey(keyBytes []byte, passphrase string) ([]byte, error) {
145+
// this section makes some small changes to code from notary/tuf/utils/x509.go
146+
pemBlock, _ := pem.Decode(keyBytes)
147+
if pemBlock == nil {
148+
return nil, fmt.Errorf("no valid private key found")
149+
}
150+
151+
var err error
152+
if x509.IsEncryptedPEMBlock(pemBlock) {
153+
keyBytes, err = x509.DecryptPEMBlock(pemBlock, []byte(passphrase))
154+
if err != nil {
155+
return nil, errors.Wrap(err, "private key is encrypted, but could not decrypt it")
156+
}
157+
keyBytes = pem.EncodeToMemory(&pem.Block{Type: pemBlock.Type, Bytes: keyBytes})
158+
}
159+
160+
return keyBytes, nil
161+
}
162+
163+
// getCert returns a Certificate from the CertFile and KeyFile in 'options',
164+
// if the key is encrypted, the Passphrase in 'options' will be used to
165+
// decrypt it.
166+
func getCert(options Options) ([]tls.Certificate, error) {
167+
if options.CertFile == "" && options.KeyFile == "" {
168+
return nil, nil
169+
}
170+
171+
errMessage := "Could not load X509 key pair"
172+
173+
cert, err := ioutil.ReadFile(options.CertFile)
174+
if err != nil {
175+
return nil, errors.Wrap(err, errMessage)
176+
}
177+
178+
prKeyBytes, err := ioutil.ReadFile(options.KeyFile)
179+
if err != nil {
180+
return nil, errors.Wrap(err, errMessage)
181+
}
182+
183+
prKeyBytes, err = getPrivateKey(prKeyBytes, options.Passphrase)
184+
if err != nil {
185+
return nil, errors.Wrap(err, errMessage)
186+
}
187+
188+
tlsCert, err := tls.X509KeyPair(cert, prKeyBytes)
189+
if err != nil {
190+
return nil, errors.Wrap(err, errMessage)
191+
}
192+
193+
return []tls.Certificate{tlsCert}, nil
194+
}
195+
130196
// Client returns a TLS configuration meant to be used by a client.
131197
func Client(options Options) (*tls.Config, error) {
132198
tlsConfig := ClientDefault()
@@ -139,13 +205,11 @@ func Client(options Options) (*tls.Config, error) {
139205
tlsConfig.RootCAs = CAs
140206
}
141207

142-
if options.CertFile != "" || options.KeyFile != "" {
143-
tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile)
144-
if err != nil {
145-
return nil, fmt.Errorf("Could not load X509 key pair: %v. Make sure the key is not encrypted", err)
146-
}
147-
tlsConfig.Certificates = []tls.Certificate{tlsCert}
208+
tlsCerts, err := getCert(options)
209+
if err != nil {
210+
return nil, err
148211
}
212+
tlsConfig.Certificates = tlsCerts
149213

150214
if err := adjustMinVersion(options, tlsConfig); err != nil {
151215
return nil, err

tlsconfig/config_test.go

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
4242
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
4343
-----END CERTIFICATE-----
4444
`
45-
rsaPrivateKeyFile = "fixtures/key.pem"
46-
certificateFile = "fixtures/cert.pem"
47-
multiCertificateFile = "fixtures/multi.pem"
45+
rsaPrivateKeyFile = "fixtures/key.pem"
46+
certificateFile = "fixtures/cert.pem"
47+
multiCertificateFile = "fixtures/multi.pem"
48+
rsaEncryptedPrivateKeyFile = "fixtures/encrypted_key.pem"
49+
certificateOfEncryptedKeyFile = "fixtures/cert_of_encrypted_key.pem"
4850
)
4951

5052
// returns the name of a pre-generated, multiple-certificate CA file
@@ -58,6 +60,12 @@ func getCertAndKey() (string, string) {
5860
return rsaPrivateKeyFile, certificateFile
5961
}
6062

63+
// returns the names of pre-generated, encrypted private key and
64+
// corresponding certificate file
65+
func getCertAndEncryptedKey() (string, string) {
66+
return rsaEncryptedPrivateKeyFile, certificateOfEncryptedKeyFile
67+
}
68+
6169
// If the cert files and directory are provided but are invalid, an error is
6270
// returned.
6371
func TestConfigServerTLSFailsIfUnableToLoadCerts(t *testing.T) {
@@ -478,6 +486,42 @@ func TestConfigClientTLSValidClientCertAndKey(t *testing.T) {
478486
}
479487
}
480488

489+
// The certificate is set if the client cert and encrypted client key are
490+
// provided and valid and passphrase can decrypt the key
491+
func TestConfigClientTLSValidClientCertAndEncryptedKey(t *testing.T) {
492+
key, cert := getCertAndEncryptedKey()
493+
494+
tlsConfig, err := Client(Options{
495+
CertFile: cert,
496+
KeyFile: key,
497+
Passphrase: "FooBar123",
498+
})
499+
500+
if err != nil || tlsConfig == nil {
501+
t.Fatal("Unable to configure client TLS", err)
502+
}
503+
504+
if len(tlsConfig.Certificates) != 1 {
505+
t.Fatal("Unexpected client certificates")
506+
}
507+
}
508+
509+
// The certificate is not set if the provided passphrase cannot decrypt
510+
// the encrypted key.
511+
func TestConfigClientTLSNotSetWithInvalidPassphrase(t *testing.T) {
512+
key, cert := getCertAndEncryptedKey()
513+
514+
tlsConfig, err := Client(Options{
515+
CertFile: cert,
516+
KeyFile: key,
517+
Passphrase: "InvalidPassphrase",
518+
})
519+
520+
if !IsErrEncryptedKey(err) || tlsConfig != nil {
521+
t.Fatal("Expected failure due to incorrect passphrase.")
522+
}
523+
}
524+
481525
// Exclusive root pools determines whether the CA pool will be a union of the system
482526
// certificate pool and custom certs, or an exclusive or of the custom certs and system pool
483527
func TestConfigClientExclusiveRootPools(t *testing.T) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIC1jCCAb6gAwIBAgIDAw0/MA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMTBHRl
3+
c3QwHhcNMTYwNDIyMDQyMjM1WhcNMTgwNDIyMDQyMjM1WjAPMQ0wCwYDVQQDEwR0
4+
ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4GRTos+Ik6kQG7wn
5+
8E4HqPwgWXbY0T59UQsrbR+YbyxbUKV67Pgl4VImuUmYaism6Tm3EFYzeom5baMc
6+
vW0hC+WbwVr1rq5ddBE8akYhlPY40SxFlh563vOi7lcFGM7xuUbTlhtAhYa5xc5U
7+
thHYa8Mdqc2kMrmU4JBhNHoRk2mnRBo2J2/8RfOfioM6mH0t/MVtB/jSGpcwbbfj
8+
2twKOpB9CoX57szVo7+DCFHpLxeuop+69REu5Egc2a5BtBuUf0fkUBKuF7yUy2xI
9+
IbgjCiGb3Z+PCIC0CjNt9wExowPAGfxAJ8s1nNlpZav3707VZRtz7Js1skRjm9aU
10+
8fhYNQIDAQABozswOTAOBgNVHQ8BAf8EBAMCBaAwGQYDVR0lBBIwEAYIKwYBBQUH
11+
AwMGBFUdJQAwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAcKCCV5Os
12+
O2U7Ekp0jzOusV2+ykZzUe4sEds+ikblxK9SHV/pAPIVuAevdyE1LKmJ6ZGgeU2M
13+
4MC6jC/XTlYNhsYCfKaJn53UscKI2urXFlk1Gv5VQP5EOrMWb76A5uj1nElxKe2C
14+
bMVoUuMwRd9jnz6594D80jGGYpHRaF7yLtGbiflDjB+yv1OU6WnuVNr0nOb9ShR6
15+
WPlrQj5TUSpRHF/oKy9LVWuxYA9aiY1YREDZhhauw9pGAMx1lImfJcJ077MdxN4A
16+
DwKAx3ooajAu1n3McY1oncWW+rWs2Ptvp6lKMGoZ50ElEPCMw4/hPtPMLq/DTWNj
17+
l342KLVWgchlIA==
18+
-----END CERTIFICATE-----

tlsconfig/fixtures/encrypted_key.pem

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
Proc-Type: 4,ENCRYPTED
3+
DEK-Info: AES-256-CBC,68ce1d54f187b663e152d9c5dc900fd3
4+
5+
ZVBeXx7kWiF0yPOORntrN6BsyIJE7krqTVhRfk6GAllaLQv0jvb31XHB1oWOaqnx
6+
tb7kUuoBeQdl1hs/iAnkDMc59WJfEK9A9cAD/SgxTgdENOrzFSRNEfqketLA4eHZ
7+
2sOLkSfv58HwA0p0gzqSrLQBo/6ZtF/57HxH166PtErPNTS1Usu/f4Oj0UqxTfbZ
8+
B5LHsepyNLt6q/15fcY0TFYJwvgEXa4SridjT+8bTz2T+bx3QFijGnl7EdkTElni
9+
FIwnDjFZaAULqoyUIB1y8guEZVkaWKncxPdRfhId84HklWdrrLtP5D6db1xNNpsp
10+
LzGdciD3phJp6K0hpl+WrhYxuCKURa27tXMCuYOFd1hw/kM29jFbxSIlNBGN4OLL
11+
v4wYrJFM21iWsz9c7Cqw5Yls2Rsx0QrXRFIxwT25z+HNx1fysQxYuxf3r+e2oz8e
12+
8Os7hvcxG2XDz01/zpx8kzxUcLuh+3o5UOYlo9z6qsjaD5NUXY+X90PUrVO9fk5y
13+
8o8pnElPnV88Ihrog5YTYy6egiQWHhDk2I4qlYPOBQNKTLg3KulAcmC9vQ8mR5Sy
14+
p3c3MTgh0A3Zk5Dib+sQ0tdbwDcB2JCTqGal1FNEW5Z7qTHA4Bdm2l7hGs8cRpy4
15+
Ehkhv3s5wWmKcbwwlPuJ0UfPeDn6v9qE2/IkOy+jWgTpaFyWtXHc1/XdqMsJ8xN0
16+
thJw/GMtNabB1+zuayJnvmbJd2qW1smsFTHqX3BovXIH4vx1hE2d0lJpEBynk+wr
17+
gpPgrRoEiqsPcsRoVjvKH3qwJLRdcGYhKqhbvRdynlagCLmE8iAI99r82u6t+03h
18+
YNpRbafY4ceAYyK0IlRiJvGkBMfH7bMXcBMmXyQSBF27ZpNidyZSCHrU5xyHqJZO
19+
XWUhl9GHplBfueh5E831S7mDqobd8RqnUvKVygyEOol5VUFDrggTAAKKN9VzM3uT
20+
MaVymt6fA7stzf01fT+Wi7uCm5legTXG3Ca+XxD6TdE0dNzewd5jDsuqwXnt1iC4
21+
slvuLRZeRZDNvBd0G7Ohhp6jb2HHwkv9kQTZ+UEDbR/Gwxty4oT1MnwSE0mi9ZFN
22+
6PTjrSxpIKe+mAhgzrepLMfATGayYQzucEArPG7Vp+NJva+j6FKloqrzXMjlP0hN
23+
XSBr7AL+j+OR/tzDOoUG3xdsCl/u5hFTpjsW2ti870zoRUcK0fqJ9UIYjh66L7yT
24+
KNkXsC+OcGuGkhtQ0gxx60OI7wp4bh2pKdT6e111/WTvXxVR2C3XhFBLUfNIz/7A
25+
Oj+s0CaV4pBmCjIobLYpxC0ofLplwBLGf9xnsBiQF5dsgKgOhACeDmDMwqAJ3U/t
26+
54hK/8Yb8W46Tjgbm0Qsj5gFXHofnyqDeQxAjsdCXsdMaPB8nyZpEkuQSEj9HlKW
27+
xIEErVufkvqyrzhX1pxPs+C839Ueyeob6ZWQurqCLTdZh+3bhKcvi5iP+aLLjMWK
28+
JT9tmAuFVkbPerqObVQFbnM4/re33YYD7QXCqta5bxcVeBI8N1HdwMYrDVhXelEx
29+
mqGleUkkDHTWzAa3u1GKOzLXAYnD0TsTwml0+k+Rf0QMBiDJiKujfy7fGqfZF2vR
30+
-----END RSA PRIVATE KEY-----

0 commit comments

Comments
 (0)