Browse code

Login must ignore some SSL cert errors when --insecure

Fabiano Franz authored on 2016/09/29 05:18:41
Showing 4 changed files
... ...
@@ -2,6 +2,7 @@ package login
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"crypto/x509"
5 6
 	"fmt"
6 7
 	"io"
7 8
 	"net"
... ...
@@ -80,10 +81,23 @@ func dialToServer(clientConfig restclient.Config) error {
80 80
 	return nil
81 81
 }
82 82
 
83
-func promptForInsecureTLS(reader io.Reader, out io.Writer) bool {
83
+func promptForInsecureTLS(reader io.Reader, out io.Writer, reason error) bool {
84
+	var insecureTLSRequestReason string
85
+	if reason != nil {
86
+		switch reason.(type) {
87
+		case x509.UnknownAuthorityError:
88
+			insecureTLSRequestReason = "The server uses a certificate signed by an unknown authority."
89
+		case x509.HostnameError:
90
+			insecureTLSRequestReason = fmt.Sprintf("The server is using a certificate that does not match its hostname: %s", reason.Error())
91
+		case x509.CertificateInvalidError:
92
+			insecureTLSRequestReason = fmt.Sprintf("The server is using an invalid certificate: %s", reason.Error())
93
+		}
94
+	}
84 95
 	var input bool
85 96
 	if term.IsTerminal(reader) {
86
-		fmt.Fprintln(out, "The server uses a certificate signed by an unknown authority.")
97
+		if len(insecureTLSRequestReason) > 0 {
98
+			fmt.Fprintln(out, insecureTLSRequestReason)
99
+		}
87 100
 		fmt.Fprintln(out, "You can bypass the certificate check, but any data you send to the server could be intercepted by others.")
88 101
 		input = cmdutil.PromptForBool(os.Stdin, out, "Use insecure connections? (y/n): ")
89 102
 		fmt.Fprintln(out)
... ...
@@ -121,10 +121,10 @@ func (o *LoginOptions) getClientConfig() (*restclient.Config, error) {
121 121
 		// certificate authority unknown, check or prompt if we want an insecure
122 122
 		// connection or if we already have a cluster stanza that tells us to
123 123
 		// connect to this particular server insecurely
124
-		case x509.UnknownAuthorityError:
124
+		case x509.UnknownAuthorityError, x509.HostnameError, x509.CertificateInvalidError:
125 125
 			if o.InsecureTLS ||
126 126
 				hasExistingInsecureCluster(*clientConfig, *o.StartingKubeConfig) ||
127
-				promptForInsecureTLS(o.Reader, o.Out) {
127
+				promptForInsecureTLS(o.Reader, o.Out, err) {
128 128
 				clientConfig.Insecure = true
129 129
 				clientConfig.CAFile = ""
130 130
 				clientConfig.CAData = nil
... ...
@@ -1,15 +1,21 @@
1 1
 package login
2 2
 
3 3
 import (
4
+	"crypto/tls"
5
+	"fmt"
4 6
 	"net/http"
5 7
 	"net/http/httptest"
8
+	"regexp"
6 9
 	"strings"
7 10
 	"testing"
8 11
 
12
+	"github.com/MakeNowJust/heredoc"
13
+
9 14
 	"github.com/openshift/origin/pkg/cmd/cli/config"
10 15
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
11 16
 
12 17
 	"k8s.io/kubernetes/pkg/client/restclient"
18
+	kclientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
13 19
 )
14 20
 
15 21
 func TestNormalizeServerURL(t *testing.T) {
... ...
@@ -55,6 +61,163 @@ func TestNormalizeServerURL(t *testing.T) {
55 55
 	}
56 56
 }
57 57
 
58
+func TestTLSWithCertificateNotMatchingHostname(t *testing.T) {
59
+	// generated by 'go run src/crypto/tls/generate_cert.go --rsa-bits 1024 --host invalidhost.com,8.8.8.8 --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h'
60
+	invalidHostCert := heredoc.Doc(`
61
+		-----BEGIN CERTIFICATE-----
62
+		MIICBjCCAW+gAwIBAgIRALOIWXyeLzunaiVkP2itHAEwDQYJKoZIhvcNAQELBQAw
63
+		EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
64
+		MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
65
+		gYkCgYEAuKDlC4aMBbHaXgS+MFud5h3zeE4boSqKgFI6HceySF/a+qg0v+ID6EwQ
66
+		DpJ2W5AdJGEBfixo+tym6q3oKWHJUX0hInkJ6dXIdUbVOeO5dIsGG0fZmRD7DDDx
67
+		snkXrDB/E0JglHNckRbIh/jvznbDfbddIcdgZ7JVIfnNpigtHZECAwEAAaNaMFgw
68
+		DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
69
+		MAMBAf8wIAYDVR0RBBkwF4IPaW52YWxpZGhvc3QuY29thwQICAgIMA0GCSqGSIb3
70
+		DQEBCwUAA4GBAAkPU044aFkBl4f/muwSh/oPGinnA4fp8ei0KMnLk+0/CjNb3Waa
71
+		GtuRVIudRTK2M/RzdpUrwfWlVmkezV4BR1K/aOH9a29zqDTkEjnkIbWwe+piAs+w
72
+		VxIxrTqM8rqq8qxeWS54AyF/OaLJgXzDpCFnCb7kY3iyHv6lcmCjluLW
73
+		-----END CERTIFICATE-----`)
74
+	invalidHostKey := heredoc.Doc(`
75
+		-----BEGIN RSA PRIVATE KEY-----
76
+		MIICXQIBAAKBgQC4oOULhowFsdpeBL4wW53mHfN4ThuhKoqAUjodx7JIX9r6qDS/
77
+		4gPoTBAOknZbkB0kYQF+LGj63KbqregpYclRfSEieQnp1ch1RtU547l0iwYbR9mZ
78
+		EPsMMPGyeResMH8TQmCUc1yRFsiH+O/OdsN9t10hx2BnslUh+c2mKC0dkQIDAQAB
79
+		AoGAZ0ZAuNC7NFhHEL5QcJZe3aC1Vv9B/0XfkWXtckkJFejggcNjNk5D50Xc2Xnd
80
+		0NvtITNN9Xj8BA83IyDCM5uqUwDbOLIc6qYgAGWzxZZSDAQg1iOAAZoXmMTNS6Zf
81
+		hQhNUIwB68ELGvbcq7cxQL7L9n4GfISz7PKOOUKTZp0Q8G0CQQD07K7NES340c3I
82
+		QVkCW5/ygNK0GuQ8nTcG5yC8R5SDS47N8YzPp17Pajah8+wawYiemY1fUmD7P/bq
83
+		Cjl2RtIHAkEAwPo1GzJubN7PSYgPir3TxUGtMJoyc3jfdjblXyGJHwTu2YxeRjd2
84
+		YUPVRpu9JvNjZc+GONvTbTZeNWCvy0JNpwJBAKEsi49JCd6eefOZBTDnCKd1nLKG
85
+		q8Ezl/2D5WfhFtsbwrrFhOs1cc++Tnte3/VvfC8aTwz2UfmkyyCSX+P0kMsCQCIL
86
+		glb7/LNEU7mbQXKurq+8OHu8mG36wyGt6aVw2yoXyrOiqfclTcM3HmdIjoRSqBSM
87
+		Ghfp4FECKHiuSBVJ6z0CQQDF37CRpdQRDPnAedhyApLcIxSbYo1oUm7FxBLyVb7V
88
+		HQjFvsOylsSCABXz0FyC7zXQxkEo6CiSahVI/PHz6Zta
89
+		-----END RSA PRIVATE KEY-----`)
90
+
91
+	server, err := newTLSServer(invalidHostCert, invalidHostKey)
92
+	if err != nil {
93
+		t.Errorf(err.Error())
94
+	}
95
+	server.StartTLS()
96
+	defer server.Close()
97
+
98
+	testCases := map[string]struct {
99
+		serverURL      string
100
+		skipTLSVerify  bool
101
+		expectedErrMsg *regexp.Regexp
102
+	}{
103
+		"succeed skipping tls": {
104
+			serverURL:     server.URL,
105
+			skipTLSVerify: true,
106
+		},
107
+		"certificate hostname doesn't match": {
108
+			serverURL:      server.URL,
109
+			expectedErrMsg: regexp.MustCompile(`The server is using a certificate that does not match its hostname(.*)is valid for 8\.8\.8\.8`),
110
+		},
111
+	}
112
+
113
+	for name, test := range testCases {
114
+		t.Logf("evaluating test: %s", name)
115
+		options := &LoginOptions{
116
+			Server:             test.serverURL,
117
+			InsecureTLS:        test.skipTLSVerify,
118
+			StartingKubeConfig: &kclientcmdapi.Config{},
119
+		}
120
+
121
+		if _, err = options.getClientConfig(); err != nil {
122
+			if !test.expectedErrMsg.MatchString(err.Error()) {
123
+				t.Errorf("%s: expected error %q but got %q", name, test.expectedErrMsg, err)
124
+			}
125
+			if test.expectedErrMsg == nil {
126
+				t.Errorf("%s: unexpected error: %v", name, err)
127
+			}
128
+		} else {
129
+			if test.expectedErrMsg != nil {
130
+				t.Errorf("%s: expected error but got nothing", name)
131
+			}
132
+		}
133
+	}
134
+}
135
+
136
+func TestTLSWithExpiredCertificate(t *testing.T) {
137
+	// generated by 'go run src/crypto/tls/generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1h'
138
+	expiredCert := heredoc.Doc(`
139
+		-----BEGIN CERTIFICATE-----
140
+		MIICEjCCAXugAwIBAgIRALf82bYpro/jQS8fP74dG5EwDQYJKoZIhvcNAQELBQAw
141
+		EjEQMA4GA1UEChMHQWNtZSBDbzAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEwMTAw
142
+		MDBaMBIxEDAOBgNVBAoTB0FjbWUgQ28wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
143
+		AoGBAONNgDXBk2Q1i/aJjTwt03KpQ3nQblMS3IX/H9JWw6ta6UublKBOaD/2o5Xt
144
+		FM+Q7XDEnzYw88CK5KHdyejkJo5IBpUjQYJZFzUJ1BC8Lw7yy6dXWYBJboRR1S+1
145
+		JhkMJOtpPecv+4cTaynplYj0WMBjcQthg2RM7tdpyUYpsp2rAgMBAAGjaDBmMA4G
146
+		A1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTAD
147
+		AQH/MC4GA1UdEQQnMCWCC2V4YW1wbGUuY29thwR/AAABhxAAAAAAAAAAAAAAAAAA
148
+		AAABMA0GCSqGSIb3DQEBCwUAA4GBAFpdiiM5YAQQN0H5ZMNuHWGlprjp7qVilO8/
149
+		WFePZRWY2vQF8g7/c1cX4bPqG+qFJd+9j2UZNjhadNfMCxvu6BY7NCupOHVHmnRQ
150
+		ocvkPoSqobE7qDPfiUuU1J+61Libu6b2IjV3/K9pvZkLiBrqn0YhoXXa0PG+rG1L
151
+		9X7+mb5z
152
+		-----END CERTIFICATE-----`)
153
+	expiredKey := heredoc.Doc(`
154
+		-----BEGIN RSA PRIVATE KEY-----
155
+		MIICXQIBAAKBgQDjTYA1wZNkNYv2iY08LdNyqUN50G5TEtyF/x/SVsOrWulLm5Sg
156
+		Tmg/9qOV7RTPkO1wxJ82MPPAiuSh3cno5CaOSAaVI0GCWRc1CdQQvC8O8sunV1mA
157
+		SW6EUdUvtSYZDCTraT3nL/uHE2sp6ZWI9FjAY3ELYYNkTO7XaclGKbKdqwIDAQAB
158
+		AoGBAJPFWKqZ9CZboWhfuE/9Qs/yNonE9VRQmMkMOTXXblHCQpUCyjcFgkTDJUpc
159
+		3QCsKZD8Yr0qSe1M3qJUu+UKHf18LqwiL/ynnalYggxIFS5/SidWCngKvIuEfkLK
160
+		VsnCK3jt5qx21iljGHU6bQZHnHB9IGEiBYcnQlvvw/WdvRDBAkEA8/KMpJVwnI1W
161
+		7fzcZ1+mbMeSJoAVIa9u7MgI+LIRZMokDRYeAMvEjm3GYpZDqA5l1dp7KochMep/
162
+		0vSSTHt7ewJBAO6IbcUIDhXuh2qdxR/Xk5DdDCoxaD1o4ivyj9JsSlGa9JWD7kKN
163
+		6ZFFrn8i7uQuniC1Rwc/4yHhs6OqbiF695ECQQCBwVKzvFUwwDEr1yK4zXStSZ3g
164
+		YqJaz4CV63RyK+z6ilaQq2H8FGaRR6yNBdYozre1/0ciAMxUS6H/6Fzk141/AkBe
165
+		SguqIP8AaGObH3Z2mc65KsfOPe2IqNcOrDlx4mCWVXxtRdN+933mcPcDRpnMFSlo
166
+		oH/NO9Ha6M8L2SjjjyohAkBJHU61+OWz/TAy1nxsMbFsISLn/JrdEZIf2uFORlDN
167
+		Z3/XIQ+yeg4Jk1VbTMZ0/fHf9xMFR8acC/7n7jxnzQau
168
+		-----END RSA PRIVATE KEY-----`)
169
+
170
+	server, err := newTLSServer(expiredCert, expiredKey)
171
+	if err != nil {
172
+		t.Errorf(err.Error())
173
+	}
174
+	server.StartTLS()
175
+	defer server.Close()
176
+
177
+	testCases := map[string]struct {
178
+		serverURL      string
179
+		skipTLSVerify  bool
180
+		expectedErrMsg *regexp.Regexp
181
+	}{
182
+		"succeed skipping tls": {
183
+			serverURL:     server.URL,
184
+			skipTLSVerify: true,
185
+		},
186
+		"certificate expired": {
187
+			serverURL:      server.URL,
188
+			expectedErrMsg: regexp.MustCompile(`The server is using an invalid certificate(.*)has expired`),
189
+		},
190
+	}
191
+
192
+	for name, test := range testCases {
193
+		t.Logf("evaluating test: %s", name)
194
+		options := &LoginOptions{
195
+			Server:             test.serverURL,
196
+			InsecureTLS:        test.skipTLSVerify,
197
+			StartingKubeConfig: &kclientcmdapi.Config{},
198
+		}
199
+
200
+		if _, err = options.getClientConfig(); err != nil {
201
+			if !test.expectedErrMsg.MatchString(err.Error()) {
202
+				t.Errorf("%s: expected error %q but got %q", name, test.expectedErrMsg, err)
203
+			}
204
+			if test.expectedErrMsg == nil {
205
+				t.Errorf("%s: unexpected error: %v", name, err)
206
+			}
207
+		} else {
208
+			if test.expectedErrMsg != nil {
209
+				t.Errorf("%s: expected error but got nothing", name)
210
+			}
211
+		}
212
+	}
213
+}
214
+
58 215
 func TestDialToHTTPServer(t *testing.T) {
59 216
 	invoked := make(chan struct{}, 1)
60 217
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
... ...
@@ -92,3 +255,60 @@ func TestDialToHTTPServer(t *testing.T) {
92 92
 		}
93 93
 	}
94 94
 }
95
+
96
+func TestDialToHTTPSServer(t *testing.T) {
97
+	invoked := make(chan struct{}, 1)
98
+	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99
+		invoked <- struct{}{}
100
+		w.WriteHeader(http.StatusOK)
101
+	}))
102
+	defer server.Close()
103
+
104
+	testCases := map[string]struct {
105
+		serverURL       string
106
+		skipTLSVerify   bool
107
+		evalExpectedErr func(error) bool
108
+	}{
109
+		"succeed dialing": {
110
+			serverURL:     server.URL,
111
+			skipTLSVerify: true,
112
+		},
113
+		"certificate unknown": {
114
+			serverURL:       server.URL,
115
+			evalExpectedErr: clientcmd.IsCertificateAuthorityUnknown,
116
+		},
117
+	}
118
+
119
+	for name, test := range testCases {
120
+		t.Logf("evaluating test: %s", name)
121
+		clientConfig := &restclient.Config{
122
+			Host:     test.serverURL,
123
+			Insecure: test.skipTLSVerify,
124
+		}
125
+		if err := dialToServer(*clientConfig); err != nil {
126
+			if test.evalExpectedErr == nil || !test.evalExpectedErr(err) {
127
+				t.Errorf("%s: unexpected error: %v", name, err)
128
+			}
129
+		} else {
130
+			if test.evalExpectedErr != nil {
131
+				t.Errorf("%s: expected error but got nothing", name)
132
+			}
133
+		}
134
+	}
135
+}
136
+
137
+func newTLSServer(certString, keyString string) (*httptest.Server, error) {
138
+	invoked := make(chan struct{}, 1)
139
+	server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
140
+		invoked <- struct{}{}
141
+		w.WriteHeader(http.StatusOK)
142
+	}))
143
+	cert, err := tls.X509KeyPair([]byte(certString), []byte(keyString))
144
+	if err != nil {
145
+		return nil, fmt.Errorf("error configuring server cert: %s", err)
146
+	}
147
+	server.TLS = &tls.Config{
148
+		Certificates: []tls.Certificate{cert},
149
+	}
150
+	return server, nil
151
+}
... ...
@@ -1,6 +1,7 @@
1 1
 package clientcmd
2 2
 
3 3
 import (
4
+	"crypto/x509"
4 5
 	"errors"
5 6
 	"fmt"
6 7
 	"strings"
... ...
@@ -13,6 +14,8 @@ const (
13 13
 	unknownReason = iota
14 14
 	noServerFoundReason
15 15
 	certificateAuthorityUnknownReason
16
+	certificateHostnameErrorReason
17
+	certificateInvalidReason
16 18
 	configurationInvalidReason
17 19
 	tlsOversizedRecordReason
18 20
 
... ...
@@ -50,6 +53,12 @@ func GetPrettyMessageForServer(err error, serverName string) string {
50 50
 			serverName = "server"
51 51
 		}
52 52
 		return fmt.Sprintf(tlsOversizedRecordMsg, err, serverName)
53
+
54
+	case certificateHostnameErrorReason:
55
+		return fmt.Sprintf("The server is using a certificate that does not match its hostname: %s", err)
56
+
57
+	case certificateInvalidReason:
58
+		return fmt.Sprintf("The server is using an invalid certificate: %s", err)
53 59
 	}
54 60
 
55 61
 	return err.Error()
... ...
@@ -91,6 +100,17 @@ func IsTLSOversizedRecord(err error) bool {
91 91
 	return detectReason(err) == tlsOversizedRecordReason
92 92
 }
93 93
 
94
+// IsCertificateHostnameError checks whether the set of authorized names doesn't match the requested name
95
+func IsCertificateHostnameError(err error) bool {
96
+	return detectReason(err) == certificateHostnameErrorReason
97
+}
98
+
99
+// IsCertificateInvalid checks whether the certificate is invalid for reasons like expired,	CA not authorized
100
+// to sign, there are too many cert intermediates, or the cert usage is not valid for the wanted purpose.
101
+func IsCertificateInvalid(err error) bool {
102
+	return detectReason(err) == certificateInvalidReason
103
+}
104
+
94 105
 func detectReason(err error) int {
95 106
 	if err != nil {
96 107
 		switch {
... ...
@@ -103,6 +123,14 @@ func detectReason(err error) int {
103 103
 		case strings.Contains(err.Error(), "tls: oversized record received"):
104 104
 			return tlsOversizedRecordReason
105 105
 		}
106
+		switch err.(type) {
107
+		case x509.UnknownAuthorityError:
108
+			return certificateAuthorityUnknownReason
109
+		case x509.HostnameError:
110
+			return certificateHostnameErrorReason
111
+		case x509.CertificateInvalidError:
112
+			return certificateInvalidReason
113
+		}
106 114
 	}
107 115
 	return unknownReason
108 116
 }