Skip to content

Commit df60a20

Browse files
authored
feat: Update TLS validation to use both SAN and CN fields. (#979)
This updates the logic used by the connector to validate server certificates. When connecting to the instance, the connector's TLS validator will first check the SAN field, and then if that fails check the CN field in the certificate for the instance name. This will enable the connector to work smoothly with both legacy and newer instances. In the Go TLS library, the only way to replace the TLS hostname verification check is to set TLSConfig.InsecureSkipVerify = true, and then replace the entire TLS verification function by setting TLSConfig.VerifyPeerCertificate. To summarize the deviations from standard TLS hostname verification: Historically, Cloud SQL creates server certificates with the instance name in the Subject.CN field in the format "my-project:my-instance". The connector is expected to check that the instance name that the connector was configured to dial matches the server certificate Subject.CN field. Thus, the Subject.CN field for most Cloud SQL instances does not contain a well-formed DNS Name. This breaks standard TLS hostname verification. Also, there are times when the instance metadata reports that an instance has a DNS name, but that DNS name does not yet appear in the SAN records of the server certificate. The client should fall back to use the instance name in the Subject.CN field to validate the server certificate's identity.
1 parent f3cd4df commit df60a20

File tree

7 files changed

+437
-99
lines changed

7 files changed

+437
-99
lines changed

dialer_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,67 @@ func TestDialerChecksSubjectAlternativeNameAndFails(t *testing.T) {
12221222
}
12231223
}
12241224

1225+
func TestDialerChecksSubjectAlternativeNameAndFallsBackToCN(t *testing.T) {
1226+
1227+
// Create an instance with custom SAN 'db.example.com'
1228+
inst := mock.NewFakeCSQLInstance(
1229+
"myProject", "myRegion", "myInstance",
1230+
mock.WithDNS("db.example.com"),
1231+
mock.WithMissingSAN("db.example.com"), // don't put db.example.com in the server cert.
1232+
)
1233+
1234+
// resolve db.example.com to the same instance
1235+
wantName, _ := instance.ParseConnNameWithDomainName("myProject:myRegion:myInstance", "db.example.com")
1236+
1237+
d := setupDialer(t, setupConfig{
1238+
testInstance: inst,
1239+
reqs: []*mock.Request{
1240+
mock.InstanceGetSuccess(inst, 1),
1241+
mock.CreateEphemeralSuccess(inst, 1),
1242+
},
1243+
1244+
dialerOptions: []Option{
1245+
WithTokenSource(mock.EmptyTokenSource{}),
1246+
WithResolver(&fakeResolver{
1247+
entries: map[string]instance.ConnName{
1248+
"db.example.com": wantName,
1249+
"myProject:myRegion:myInstance": wantName,
1250+
},
1251+
}),
1252+
},
1253+
})
1254+
1255+
tcs := []struct {
1256+
desc string
1257+
icn string
1258+
}{
1259+
{
1260+
desc: "Fallback from connect with Instance Connection Name",
1261+
icn: "myProject:myRegion:myInstance",
1262+
},
1263+
{
1264+
desc: "Fallback from connect with configured domain name",
1265+
icn: "db.example.com",
1266+
},
1267+
}
1268+
for _, tc := range tcs {
1269+
t.Run(tc.desc, func(t *testing.T) {
1270+
1271+
// Dial 'db2.example.com'. This succeed overall.
1272+
// First the Hostname check will fail because the certificate does not
1273+
// contain db2.example.com
1274+
// Then the CN field check will succeed, because the instance connection
1275+
// name matches.
1276+
_, err := d.Dial(
1277+
context.Background(), tc.icn,
1278+
)
1279+
if err != nil {
1280+
t.Fatal("Want no error. Got: ", err)
1281+
}
1282+
})
1283+
}
1284+
}
1285+
12251286
func TestDialerRefreshesAfterRotateCACerts(t *testing.T) {
12261287
tcs := []struct {
12271288
desc string

internal/cloudsql/instance.go

Lines changed: 14 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -242,87 +242,28 @@ func (c ConnectionInfo) TLSConfig() *tls.Config {
242242
pool.AddCert(caCert)
243243
}
244244

245-
// If the instance metadata does not contain a domain name, use the legacy
246-
// validation checking the CN field for the instance connection name.
247-
if c.DNSName == "" {
248-
return &tls.Config{
249-
ServerName: c.ConnectionName.String(),
250-
Certificates: []tls.Certificate{c.ClientCertificate},
251-
RootCAs: pool,
252-
// We need to set InsecureSkipVerify to true due to
253-
// https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/194
254-
// https://tip.golang.org/doc/go1.11#crypto/x509
255-
//
256-
// Since we have a secure channel to the Cloud SQL API which we use to
257-
// retrieve the certificates, we instead need to implement our own
258-
// VerifyPeerCertificate function that will verify that the certificate
259-
// is OK.
260-
InsecureSkipVerify: true,
261-
VerifyPeerCertificate: verifyPeerCertificateFunc(c.ConnectionName, pool),
262-
MinVersion: tls.VersionTLS13,
263-
}
264-
}
265-
266-
// If the connector was configured with a domain name, use that domain name
267-
// to validate the certificate. Otherwise, use the DNS name from the
268-
// instance metadata retrieved from the ConnectSettings API endpoint.
269-
serverName := c.ConnectionName.DomainName()
270-
if serverName == "" {
245+
var serverName string
246+
if c.ConnectionName.HasDomainName() {
247+
// If the connector was configured with a DNS name, use the DNS name from
248+
// the configuration to validate the server certificate.
249+
serverName = c.ConnectionName.DomainName()
250+
} else {
251+
// If the connector was configured with an Instance Connection Name,
252+
// use the DNS name from the instance metadata.
271253
serverName = c.DNSName
272254
}
273255

274-
// By default, use Standard TLS hostname verification name to
275-
// verify the server identity.
276256
return &tls.Config{
277257
ServerName: serverName,
278258
Certificates: []tls.Certificate{c.ClientCertificate},
279259
RootCAs: pool,
280260
MinVersion: tls.VersionTLS13,
281-
}
282-
283-
}
284-
285-
// verifyPeerCertificateFunc creates a VerifyPeerCertificate func that
286-
// verifies that the peer certificate is in the cert pool. We need to define
287-
// our own because CloudSQL instances use the instance name (e.g.,
288-
// my-project:my-instance) instead of a valid domain name for the certificate's
289-
// Common Name.
290-
func verifyPeerCertificateFunc(
291-
cn instance.ConnName, pool *x509.CertPool,
292-
) func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
293-
return func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
294-
if len(rawCerts) == 0 {
295-
return errtype.NewDialError(
296-
"no certificate to verify", cn.String(), nil,
297-
)
298-
}
299-
300-
cert, err := x509.ParseCertificate(rawCerts[0])
301-
if err != nil {
302-
return errtype.NewDialError(
303-
"failed to parse X.509 certificate", cn.String(), err,
304-
)
305-
}
306-
307-
opts := x509.VerifyOptions{Roots: pool}
308-
if _, err = cert.Verify(opts); err != nil {
309-
return errtype.NewDialError(
310-
"failed to verify certificate", cn.String(), err,
311-
)
312-
}
313-
314-
certInstanceName := fmt.Sprintf("%s:%s", cn.Project(), cn.Name())
315-
if cert.Subject.CommonName != certInstanceName {
316-
return errtype.NewDialError(
317-
fmt.Sprintf(
318-
"certificate had CN %q, expected %q",
319-
cert.Subject.CommonName, certInstanceName,
320-
),
321-
cn.String(),
322-
nil,
323-
)
324-
}
325-
return nil
261+
// Replace entire default TLS verification with our custom TLS
262+
// verification defined in verifyPeerCertificateFunc(). This allows the
263+
// connector to gracefully and securely handle deviations from standard TLS
264+
// hostname validation in some existing Cloud SQL certificates.
265+
InsecureSkipVerify: true,
266+
VerifyPeerCertificate: verifyPeerCertificateFunc(serverName, c.ConnectionName, pool),
326267
}
327268
}
328269

internal/cloudsql/instance_test.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,6 @@ func TestConnectionInfoTLSConfig(t *testing.T) {
165165
}
166166

167167
got := ci.TLSConfig()
168-
wantServerName := cn.String()
169-
if got.ServerName != wantServerName {
170-
t.Fatalf(
171-
"ConnectInfo return unexpected server name in TLS Config, "+
172-
"want = %v, got = %v",
173-
wantServerName, got.ServerName,
174-
)
175-
}
176168

177169
if got.MinVersion != tls.VersionTLS13 {
178170
t.Fatalf(
@@ -403,7 +395,7 @@ func TestConnectionInfoTLSConfigForCAS(t *testing.T) {
403395
wantRootCAs.AddCert(subCACert)
404396
// Assemble a connection info with the raw and parsed client cert
405397
// and the self-signed server certificate
406-
wantServerName := "testing dns name"
398+
wantServerName := "db.example.com"
407399
ci := ConnectionInfo{
408400
DNSName: wantServerName,
409401
ClientCertificate: tls.Certificate{
@@ -434,8 +426,8 @@ func TestConnectionInfoTLSConfigForCAS(t *testing.T) {
434426
if got.Certificates[0].Leaf != ci.ClientCertificate.Leaf {
435427
t.Fatal("leaf certificates do not match")
436428
}
437-
if got.InsecureSkipVerify {
438-
t.Fatal("InsecureSkipVerify is true, expected false")
429+
if !got.InsecureSkipVerify {
430+
t.Fatal("InsecureSkipVerify is false, expected true")
439431
}
440432
if !got.RootCAs.Equal(wantRootCAs) {
441433
t.Fatalf("unexpected root CAs, got %v, want %v", got.RootCAs, wantRootCAs)

internal/cloudsql/tls_verify.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cloudsql
16+
17+
import (
18+
"crypto/tls"
19+
"crypto/x509"
20+
"fmt"
21+
22+
"cloud.google.com/go/cloudsqlconn/errtype"
23+
"cloud.google.com/go/cloudsqlconn/instance"
24+
)
25+
26+
// verifyPeerCertificateFunc creates a VerifyPeerCertificate function with the
27+
// custom TLS verification logic to gracefully and securely handle deviations
28+
// from standard TLS hostname verification in existing Cloud SQL instance
29+
// server certificates.
30+
//
31+
// This is the verification algorithm:
32+
//
33+
// 1. Verify the server cert CA, using the CA certs from the instance metadata.
34+
// Reject the certificate if the CA is invalid.
35+
//
36+
// 2. Check that the server cert contains a SubjectAlternativeName matching the
37+
// DNS name in the connector configuration OR the DNS Name from the instance
38+
// metadata
39+
//
40+
// 3. If the SubjectAlternativeName does not match, and if the server cert
41+
// Subject.CN field is not empty, check that the Subject.CN field contains
42+
// the instance name.
43+
//
44+
// Reject the certificate if both the #2 SAN check and #3 CN checks fail.
45+
//
46+
// To summarize the deviations from standard TLS hostname verification:
47+
//
48+
// Historically, Cloud SQL creates server certificates with the instance name in
49+
// the Subject.CN field in the format "my-project:my-instance". The connector is
50+
// expected to check that the instance name that the connector was configured to
51+
// dial matches the server certificate Subject.CN field. Thus, the Subject.CN
52+
// field for most Cloud SQL instances does not contain a well-formed DNS Name.
53+
//
54+
// The default Go TLS hostname verification TLSConfig.serverName may be compared
55+
// with the Subject.CN field if Subject.CN contains a well-formed DNS name.
56+
// So the Cloud SQL server certs break the standard hostname verification in Go.
57+
// See:
58+
// - https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/194
59+
// - https://tip.golang.org/doc/go1.11#crypto/x509
60+
//
61+
// Also, there are times when the instance metadata reports that an instance has
62+
// a DNS name, but that DNS name does not yet appear in the SAN records of the
63+
// server certificate. The client should fall back to validating the hostname
64+
// using the instance name in the Subject.CN field.
65+
func verifyPeerCertificateFunc(
66+
serverName string, cn instance.ConnName, roots *x509.CertPool,
67+
) func(certs [][]byte, chain [][]*x509.Certificate) error {
68+
return func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
69+
if len(rawCerts) == 0 {
70+
return errtype.NewDialError(
71+
"no certificate to verify", cn.String(), nil,
72+
)
73+
}
74+
// Parse the raw certificates
75+
certs := make([]*x509.Certificate, 0, len(rawCerts))
76+
var err error
77+
for _, certBytes := range rawCerts {
78+
cert, err := x509.ParseCertificate(certBytes)
79+
if err != nil {
80+
return errtype.NewDialError(
81+
"failed to parse X.509 certificate", cn.String(), err,
82+
)
83+
}
84+
certs = append(certs, cert)
85+
}
86+
serverCert := certs[0]
87+
88+
// Verify the validity of the certificate chain
89+
_, err = serverCert.Verify(x509.VerifyOptions{
90+
Roots: roots,
91+
})
92+
if err != nil {
93+
err = &tls.CertificateVerificationError{
94+
UnverifiedCertificates: certs,
95+
Err: err,
96+
}
97+
return errtype.NewDialError(
98+
"failed to verify certificate", cn.String(), err,
99+
)
100+
}
101+
102+
var serverNameErr error
103+
104+
if serverName == "" {
105+
// The instance has no DNS name.
106+
// Verify only the CN
107+
return verifyCn(cn, serverCert)
108+
}
109+
110+
// The instance has a DNS name.
111+
// First, verify the server hostname
112+
serverNameErr = serverCert.VerifyHostname(serverName)
113+
if serverNameErr != nil {
114+
// If that failed, verify the CN field.
115+
cnErr := verifyCn(cn, serverCert)
116+
if cnErr != nil {
117+
// If both failed, return the server hostname error.
118+
serverNameErr = &tls.CertificateVerificationError{
119+
UnverifiedCertificates: certs,
120+
Err: serverNameErr,
121+
}
122+
return serverNameErr
123+
}
124+
}
125+
126+
// All checks passed
127+
return nil
128+
}
129+
}
130+
131+
func verifyCn(cn instance.ConnName, cert *x509.Certificate) error {
132+
// Reject CN check if the certificate CN field is empty
133+
if cert.Subject.CommonName == "" {
134+
return errtype.NewDialError(
135+
fmt.Sprintf(
136+
"certificate CN was empty, expected %q",
137+
cert.Subject.CommonName,
138+
),
139+
cn.String(),
140+
nil,
141+
)
142+
}
143+
144+
// Verify the CN field matches the instance name
145+
certInstanceName := fmt.Sprintf("%s:%s", cn.Project(), cn.Name())
146+
if cert.Subject.CommonName != certInstanceName {
147+
return errtype.NewDialError(
148+
fmt.Sprintf(
149+
"certificate had CN %q, expected %q",
150+
cert.Subject.CommonName, certInstanceName,
151+
),
152+
cn.String(),
153+
nil,
154+
)
155+
}
156+
return nil
157+
}

0 commit comments

Comments
 (0)