Browse code

Merge pull request #31364 from adshmh/30935-use-encrypted-client-certificate-to-connect-to-a-docker-host

use an encrypted client certificate to connect to a docker daemon

Sebastiaan van Stijn authored on 2017/04/10 17:42:01
Showing 4 changed files
... ...
@@ -20,6 +20,7 @@ import (
20 20
 	dopts "github.com/docker/docker/opts"
21 21
 	"github.com/docker/go-connections/sockets"
22 22
 	"github.com/docker/go-connections/tlsconfig"
23
+	"github.com/docker/notary/passphrase"
23 24
 	"github.com/pkg/errors"
24 25
 	"github.com/spf13/cobra"
25 26
 	"golang.org/x/net/context"
... ...
@@ -153,9 +154,30 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
153 153
 
154 154
 	var err error
155 155
 	cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
156
+	if tlsconfig.IsErrEncryptedKey(err) {
157
+		var (
158
+			passwd string
159
+			giveup bool
160
+		)
161
+		passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
162
+
163
+		for attempts := 0; tlsconfig.IsErrEncryptedKey(err); attempts++ {
164
+			// some code and comments borrowed from notary/trustmanager/keystore.go
165
+			passwd, giveup, err = passRetriever("private", "encrypted TLS private", false, attempts)
166
+			// Check if the passphrase retriever got an error or if it is telling us to give up
167
+			if giveup || err != nil {
168
+				return errors.Wrap(err, "private key is encrypted, but could not get passphrase")
169
+			}
170
+
171
+			opts.Common.TLSOptions.Passphrase = passwd
172
+			cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
173
+		}
174
+	}
175
+
156 176
 	if err != nil {
157 177
 		return err
158 178
 	}
179
+
159 180
 	cli.defaultVersion = cli.client.ClientVersion()
160 181
 
161 182
 	if opts.Common.TrustKey == "" {
... ...
@@ -33,7 +33,7 @@ func TestNewEnvClient(t *testing.T) {
33 33
 			envs: map[string]string{
34 34
 				"DOCKER_CERT_PATH": "invalid/path",
35 35
 			},
36
-			expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory. Make sure the key is not encrypted",
36
+			expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory",
37 37
 		},
38 38
 		{
39 39
 			envs: map[string]string{
... ...
@@ -17,7 +17,7 @@ github.com/vdemeester/shakers 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3
17 17
 golang.org/x/net c427ad74c6d7a814201695e9ffde0c5d400a7674
18 18
 golang.org/x/sys 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9
19 19
 github.com/docker/go-units 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1
20
-github.com/docker/go-connections a2afab9802043837035592f1c24827fb70766de9
20
+github.com/docker/go-connections e15c02316c12de00874640cd76311849de2aeed5
21 21
 golang.org/x/text f72d8390a633d5dfb0cc84043294db9f6c935756
22 22
 
23 23
 github.com/RackSec/srslog 456df3a81436d29ba874f3590eeeee25d666f8a5
... ...
@@ -8,11 +8,13 @@ package tlsconfig
8 8
 import (
9 9
 	"crypto/tls"
10 10
 	"crypto/x509"
11
+	"encoding/pem"
11 12
 	"fmt"
12 13
 	"io/ioutil"
13 14
 	"os"
14 15
 
15 16
 	"github.com/Sirupsen/logrus"
17
+	"github.com/pkg/errors"
16 18
 )
17 19
 
18 20
 // Options represents the information needed to create client and server TLS configurations.
... ...
@@ -34,6 +36,9 @@ type Options struct {
34 34
 	// the system pool will be used.
35 35
 	ExclusiveRootPools bool
36 36
 	MinVersion         uint16
37
+	// If Passphrase is set, it will be used to decrypt a TLS private key
38
+	// if the key is encrypted
39
+	Passphrase string
37 40
 }
38 41
 
39 42
 // 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 {
127 127
 	return nil
128 128
 }
129 129
 
130
+// IsErrEncryptedKey returns true if the 'err' is an error of incorrect
131
+// password when tryin to decrypt a TLS private key
132
+func IsErrEncryptedKey(err error) bool {
133
+	return errors.Cause(err) == x509.IncorrectPasswordError
134
+}
135
+
136
+// getPrivateKey returns the private key in 'keyBytes', in PEM-encoded format.
137
+// If the private key is encrypted, 'passphrase' is used to decrypted the
138
+// private key.
139
+func getPrivateKey(keyBytes []byte, passphrase string) ([]byte, error) {
140
+	// this section makes some small changes to code from notary/tuf/utils/x509.go
141
+	pemBlock, _ := pem.Decode(keyBytes)
142
+	if pemBlock == nil {
143
+		return nil, fmt.Errorf("no valid private key found")
144
+	}
145
+
146
+	var err error
147
+	if x509.IsEncryptedPEMBlock(pemBlock) {
148
+		keyBytes, err = x509.DecryptPEMBlock(pemBlock, []byte(passphrase))
149
+		if err != nil {
150
+			return nil, errors.Wrap(err, "private key is encrypted, but could not decrypt it")
151
+		}
152
+		keyBytes = pem.EncodeToMemory(&pem.Block{Type: pemBlock.Type, Bytes: keyBytes})
153
+	}
154
+
155
+	return keyBytes, nil
156
+}
157
+
158
+// getCert returns a Certificate from the CertFile and KeyFile in 'options',
159
+// if the key is encrypted, the Passphrase in 'options' will be used to
160
+// decrypt it.
161
+func getCert(options Options) ([]tls.Certificate, error) {
162
+	if options.CertFile == "" && options.KeyFile == "" {
163
+		return nil, nil
164
+	}
165
+
166
+	errMessage := "Could not load X509 key pair"
167
+
168
+	cert, err := ioutil.ReadFile(options.CertFile)
169
+	if err != nil {
170
+		return nil, errors.Wrap(err, errMessage)
171
+	}
172
+
173
+	prKeyBytes, err := ioutil.ReadFile(options.KeyFile)
174
+	if err != nil {
175
+		return nil, errors.Wrap(err, errMessage)
176
+	}
177
+
178
+	prKeyBytes, err = getPrivateKey(prKeyBytes, options.Passphrase)
179
+	if err != nil {
180
+		return nil, errors.Wrap(err, errMessage)
181
+	}
182
+
183
+	tlsCert, err := tls.X509KeyPair(cert, prKeyBytes)
184
+	if err != nil {
185
+		return nil, errors.Wrap(err, errMessage)
186
+	}
187
+
188
+	return []tls.Certificate{tlsCert}, nil
189
+}
190
+
130 191
 // Client returns a TLS configuration meant to be used by a client.
131 192
 func Client(options Options) (*tls.Config, error) {
132 193
 	tlsConfig := ClientDefault()
... ...
@@ -139,13 +205,11 @@ func Client(options Options) (*tls.Config, error) {
139 139
 		tlsConfig.RootCAs = CAs
140 140
 	}
141 141
 
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}
142
+	tlsCerts, err := getCert(options)
143
+	if err != nil {
144
+		return nil, err
148 145
 	}
146
+	tlsConfig.Certificates = tlsCerts
149 147
 
150 148
 	if err := adjustMinVersion(options, tlsConfig); err != nil {
151 149
 		return nil, err