Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ Go language. Using a Cloud SQL connector provides a native alternative to the

* **IAM Authorization:** uses IAM permissions to control who/what can connect to
your Cloud SQL instances
* **Improved Security:** uses robust, updated TLS 1.3 encryption and
identity verification between the client connector and the server-side proxy,
independent of the database protocol.
* **Improved Security:** uses robust, updated TLS 1.3 encryption (including
automatic [hybrid post-quantum key exchange](#post-quantum-cryptography-pqc))
and identity verification between the client connector and the server-side
proxy, independent of the database protocol.
* **Convenience:** removes the requirement to use and distribute SSL
certificates, as well as manage firewalls or source/destination IP addresses.
* (optionally) **IAM DB Authentication:** provides support for
Expand Down Expand Up @@ -452,6 +453,55 @@ dsn := "user=test-sa dbname=mydb sslmode=disable"
[configure-iam-authn]: https://cloud.google.com/sql/docs/postgres/create-edit-iam-instances#configure-iam-db-instance
[add-iam-user]: https://cloud.google.com/sql/docs/postgres/create-manage-iam-users#creating-a-database-user

### Post-Quantum Cryptography (PQC)

The Cloud SQL Go Connector establishes mTLS connections over TLS 1.3 using Go's
standard `crypto/tls` package with default curve preferences. When built with
**Go 1.24 or later** and when your application's main `go.mod` declares
**`go 1.24` or higher**, the connector automatically offers and negotiates the
hybrid post-quantum key exchange group **`X25519MLKEM768`** (ML-KEM-768 combined
with X25519) whenever the Cloud SQL instance supports it. No code changes or
dialer options are required.

#### `go.mod` language version requirement (`tlsmlkem`)

Go 1.24+ controls default `X25519MLKEM768` support through the `tlsmlkem`
`GODEBUG` setting, whose default depends on the `go` directive in your
**application's main `go.mod`**—not just the Go toolchain version used to
compile the binary:

- `cloud.google.com/go/cloudsqlconn` `v1.19.0` and later already require
`go >= 1.24` in `go.mod` (and `v1.25.3+` requires `go >= 1.26.0`), so
applications importing `v1.19.0+` automatically have `go 1.24` or higher in
their main `go.mod` and enable `X25519MLKEM768` by default.
- If your application pins an older connector release (`< v1.19.0`) and keeps
`go 1.23` or earlier in its main `go.mod`, compiling with Go 1.24+ still
defaults to `tlsmlkem=0` (classical curves only). To enable `X25519MLKEM768`
in that configuration, update your main `go.mod` directive to `go 1.24` (or
higher), add `godebug tlsmlkem=1` to `go.mod`, or add
`//go:debug tlsmlkem=1` to your `main` package.

#### Backwards compatibility

Hybrid post-quantum key exchange is backwards-compatible. When `X25519MLKEM768`
is enabled, Go's `crypto/tls` advertises both hybrid (`X25519MLKEM768`) and
classical (`X25519`, `P-256`, etc.) groups in the TLS 1.3 `ClientHello`. If a
Cloud SQL instance does not yet support `X25519MLKEM768`, the TLS handshake
automatically falls back to classical `X25519` without error.

#### Opting out

To disable post-quantum key exchange and restrict TLS handshakes to classical
curves, set the `GODEBUG` environment variable at runtime:

```sh
GODEBUG=tlsmlkem=0
```

Alternatively, configure the opt-out at build time by adding
`godebug tlsmlkem=0` to your main `go.mod` or `//go:debug tlsmlkem=0` to a file
in your `main` package.

### Enabling Metrics and Tracing

This library includes support for metrics and tracing using [OpenCensus][].
Expand Down
13 changes: 7 additions & 6 deletions dialer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ func withMockDNSResolver(r cloudsql.NetResolver) Option {

// setupConfig holds all the configuration to use when setting up a dialer.
type setupConfig struct {
testInstance mock.FakeCSQLInstance
skipServer bool
skipVerify bool
reqs []*mock.Request
dialerOptions []Option
testInstance mock.FakeCSQLInstance
skipServer bool
skipVerify bool
reqs []*mock.Request
dialerOptions []Option
serverProxyOptions []mock.ServerProxyOption
}

// setupDialer configures a Dialer with an HTTP client configured to point at a
Expand All @@ -102,7 +103,7 @@ func setupDialer(t *testing.T, c setupConfig) *Dialer {
}
stop := func() {}
if !c.skipServer {
stop = mock.StartServerProxy(t, c.testInstance)
stop = mock.StartServerProxy(t, c.testInstance, c.serverProxyOptions...)
}
t.Cleanup(func() {
stop()
Expand Down
14 changes: 14 additions & 0 deletions internal/cloudsql/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ func TestConnectionInfoTLSConfig(t *testing.T) {
)
}

if len(got.CurvePreferences) != 0 {
t.Fatalf(
"want empty CurvePreferences so crypto/tls uses default post-quantum key exchange (X25519MLKEM768), got = %v",
got.CurvePreferences,
)
}

if got.Certificates[0].Leaf != ci.ClientCertificate.Leaf {
t.Fatal("leaf certificates do not match")
}
Expand Down Expand Up @@ -527,6 +534,13 @@ func TestConnectionInfoTLSConfigForCAS(t *testing.T) {
"want CLIENT_PROTOCOL_TLS 1.3, got = %v", got.MinVersion,
)
}

if len(got.CurvePreferences) != 0 {
t.Fatalf(
"want empty CurvePreferences so crypto/tls uses default post-quantum key exchange (X25519MLKEM768), got = %v",
got.CurvePreferences,
)
}
if got.Certificates[0].Leaf != ci.ClientCertificate.Leaf {
t.Fatal("leaf certificates do not match")
}
Expand Down
48 changes: 44 additions & 4 deletions internal/mock/cloudsql.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,14 +295,43 @@ func GenerateCertWithCommonName(i FakeCSQLInstance, cn string) []byte {
return i.certs.generateServerCertWithCn(cn).Raw
}

// ServerProxyOption configures StartServerProxy.
type ServerProxyOption func(*serverProxyConfig)

type serverProxyConfig struct {
curvePreferences []tls.CurveID
onHandshake func(tls.ConnectionState)
}

// WithServerCurvePreferences configures the TLS CurvePreferences used by the
// fake server proxy. When unset, crypto/tls defaults are used.
func WithServerCurvePreferences(curves ...tls.CurveID) ServerProxyOption {
return func(c *serverProxyConfig) {
c.curvePreferences = curves
}
}

// WithServerConnectionStateHook registers a callback invoked with the server's
// tls.ConnectionState after each accepted TLS handshake completes.
func WithServerConnectionStateHook(fn func(tls.ConnectionState)) ServerProxyOption {
return func(c *serverProxyConfig) {
c.onHandshake = fn
}
}

// StartServerProxy starts a fake server proxy and listens on the provided port
// on all interfaces, configured with TLS as specified by the FakeCSQLInstance.
// Callers should invoke the returned function to clean up all resources.
func StartServerProxy(t *testing.T, i FakeCSQLInstance) func() {
func StartServerProxy(t *testing.T, i FakeCSQLInstance, opts ...ServerProxyOption) func() {
var proxyCfg serverProxyConfig
for _, opt := range opts {
opt(&proxyCfg)
}
ln, err := startListenerWithRetries(&tls.Config{
Certificates: i.certs.serverChain(i.useStandardTLSValidation),
ClientCAs: i.certs.clientCAPool(),
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: i.certs.serverChain(i.useStandardTLSValidation),
ClientCAs: i.certs.clientCAPool(),
ClientAuth: tls.RequireAndVerifyClientCert,
CurvePreferences: proxyCfg.curvePreferences,
})
if err != nil {
t.Fatalf("failed to start listener: %v", err)
Expand Down Expand Up @@ -330,6 +359,17 @@ func StartServerProxy(t *testing.T, i FakeCSQLInstance) func() {
}

go func(c net.Conn) {
if proxyCfg.onHandshake != nil {
if tlsConn, ok := c.(*tls.Conn); ok {
hErr := tlsConn.Handshake()
if hErr != nil {
t.Logf("Fake server TLS handshake error: %v", hErr)
}
if hErr == nil {
proxyCfg.onHandshake(tlsConn.ConnectionState())
}
}
}
csqlBytes := make([]byte, 8)
c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
_, rErr := c.Read(csqlBytes)
Expand Down
Loading
Loading