internal/cloudsql/tls_verify.go (83 lines of code) (raw):

// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cloudsql import ( "crypto/tls" "crypto/x509" "fmt" "cloud.google.com/go/cloudsqlconn/errtype" "cloud.google.com/go/cloudsqlconn/instance" ) // verifyPeerCertificateFunc creates a VerifyPeerCertificate function with the // custom TLS verification logic to gracefully and securely handle deviations // from standard TLS hostname verification in existing Cloud SQL instance // server certificates. // // This is the verification algorithm: // // 1. Verify the server cert CA, using the CA certs from the instance metadata. // Reject the certificate if the CA is invalid. // // 2. Check that the server cert contains a SubjectAlternativeName matching the // DNS name in the connector configuration OR the DNS Name from the instance // metadata // // 3. If the SubjectAlternativeName does not match, and if the server cert // Subject.CN field is not empty, check that the Subject.CN field contains // the instance name. // // Reject the certificate if both the #2 SAN check and #3 CN checks fail. // // 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. // // The default Go TLS hostname verification TLSConfig.serverName may be compared // with the Subject.CN field if Subject.CN contains a well-formed DNS name. // So the Cloud SQL server certs break the standard hostname verification in Go. // See: // - https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/194 // - https://tip.golang.org/doc/go1.11#crypto/x509 // // 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 validating the hostname // using the instance name in the Subject.CN field. func verifyPeerCertificateFunc( serverName string, cn instance.ConnName, roots *x509.CertPool, ) func(certs [][]byte, chain [][]*x509.Certificate) error { return func(rawCerts [][]byte, _ [][]*x509.Certificate) error { if len(rawCerts) == 0 { return errtype.NewDialError( "no certificate to verify", cn.String(), nil, ) } // Parse the raw certificates certs := make([]*x509.Certificate, 0, len(rawCerts)) var err error for _, certBytes := range rawCerts { cert, err := x509.ParseCertificate(certBytes) if err != nil { return errtype.NewDialError( "failed to parse X.509 certificate", cn.String(), err, ) } certs = append(certs, cert) } serverCert := certs[0] // Verify the validity of the certificate chain _, err = serverCert.Verify(x509.VerifyOptions{ Roots: roots, }) if err != nil { err = &tls.CertificateVerificationError{ UnverifiedCertificates: certs, Err: err, } return errtype.NewDialError( "failed to verify certificate", cn.String(), err, ) } var serverNameErr error if serverName == "" { // The instance has no DNS name. // Verify only the CN return verifyCn(cn, serverCert) } // The instance has a DNS name. // First, verify the server hostname serverNameErr = serverCert.VerifyHostname(serverName) if serverNameErr != nil { // If that failed, verify the CN field. cnErr := verifyCn(cn, serverCert) if cnErr != nil { // If both failed, return the server hostname error. serverNameErr = &tls.CertificateVerificationError{ UnverifiedCertificates: certs, Err: serverNameErr, } return serverNameErr } } // All checks passed return nil } } func verifyCn(cn instance.ConnName, cert *x509.Certificate) error { // Reject CN check if the certificate CN field is empty if cert.Subject.CommonName == "" { return errtype.NewDialError( fmt.Sprintf( "certificate CN was empty, expected %q", cert.Subject.CommonName, ), cn.String(), nil, ) } // Verify the CN field matches the instance name certInstanceName := fmt.Sprintf("%s:%s", cn.Project(), cn.Name()) if cert.Subject.CommonName != certInstanceName { return errtype.NewDialError( fmt.Sprintf( "certificate had CN %q, expected %q", cert.Subject.CommonName, certInstanceName, ), cn.String(), nil, ) } return nil }