Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,28 @@ jobs:
curl https://github.com/googleapis/repo-automation-bots/releases/download/flakybot-1.1.0/flakybot -o flakybot -s -L
chmod +x ./flakybot
./flakybot --repo ${{github.repository}} --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}

unit-pqc:
name: unit tests (OpenSSL 3.5+ PQC)
runs-on: ubuntu-latest
container:
# Explicitly pin Debian Trixie (OpenSSL 3.5+) to guarantee non-skipped
# TLS 1.3 X25519MLKEM768 wire-level coverage regardless of ubuntu-latest
# runner image shifts (ubuntu-latest currently resolves to Ubuntu 26.04,
# which also ships OpenSSL 3.5+).
image: python:3.13-trixie@sha256:6a9220edda062e9223ea0784740a535987299420b96fbfd520b89bbd3e772485
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- name: Verify OpenSSL 3.5+
run: |
python3 -c "import ssl; print(ssl.OPENSSL_VERSION); assert ssl.OPENSSL_VERSION_INFO >= (3, 5, 0), f'Expected OpenSSL >= 3.5.0, got {ssl.OPENSSL_VERSION}'"

- name: Install nox
run: pip install nox

- name: Run tests
run: nox -s unit-3.13
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,84 @@ async def main():
await pool.dispose()
```

### Post-Quantum Cryptography (PQC)

The Cloud SQL Python Connector establishes mTLS connections over TLS 1.3 using
Python's standard `ssl` module without restricting key-exchange groups. When
Python's `ssl` module is linked against **OpenSSL 3.5 or later**, TLS 1.3
connections automatically offer and negotiate 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 connector configuration
are required.

#### Checking your OpenSSL version

Post-quantum TLS key exchange depends on the OpenSSL library linked into your
Python runtime's `ssl` module (not the Python language version or the
`cryptography` package version alone). Run the following command to check which
OpenSSL version your Python interpreter uses:

```sh
python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"
```

- **OpenSSL 3.5.0 or higher** (for example, `python:3.13-trixie` / Debian 13
"Trixie" container images; check your runtime with the one-liner above):
`X25519MLKEM768` is enabled by default on TLS 1.3 connections.
- **OpenSSL earlier than 3.5.0** (for example, Ubuntu 24.04 LTS, which ships
system OpenSSL 3.0.x): TLS 1.3 connections continue to use classical elliptic
curve Diffie-Hellman (such as `X25519` or `P-256`).

> [!NOTE]
>
> Connections configured with `ip_type="sql_data"` (`IPTypes.SQL_DATA`) stream
> over gRPC (`grpcio`), which uses its bundled BoringSSL (supporting
> `X25519MLKEM768` independently of Python's `ssl` module and `OPENSSL_CONF`),
> whereas its direct-TLS fallback path uses `create_ssl_context()` and Python's
> linked OpenSSL library as described above.

#### Backwards compatibility

Hybrid post-quantum key exchange is backwards-compatible. On OpenSSL 3.5+,
Python's `ssl` module advertises both hybrid (`X25519MLKEM768`) and classical
(`X25519`, `secp256r1`, etc.) groups in the TLS 1.3 `ClientHello`. If either the
client runtime (OpenSSL `< 3.5`) or the Cloud SQL instance only supports
classical groups, the TLS 1.3 handshake automatically falls back to classical
ECDHE without error.

#### Opting out

If you need to disable post-quantum key exchange on an OpenSSL 3.5+ client (for
example, to troubleshoot a network middlebox sensitive to larger TLS
`ClientHello` records), configure OpenSSL's `Groups` list via an OpenSSL
configuration file (`OPENSSL_CONF`) to advertise only classical curves.

> [!WARNING]
>
> Setting `OPENSSL_CONF` replaces the system default OpenSSL configuration for
> the entire Python process. It affects **all** TLS connections made by the
> process (not just Cloud SQL connections), and starting from a minimal file
> omits distribution defaults (such as Debian's `MinProtocol` or
> `CipherString`). Copy your system's `openssl.cnf` (check `openssl version -d`,
> typically `/etc/ssl/openssl.cnf`) and add `Groups = X25519:P-256:P-384` under
> its `[system_default_sect]` section rather than using a bare file from
> scratch.

For example, after copying `/etc/ssl/openssl.cnf`, ensure its
`[system_default_sect]` (referenced via `ssl_conf` -> `system_default`) sets
`Groups`:

```ini
[system_default_sect]
Groups = X25519:P-256:P-384
```

Then point `OPENSSL_CONF` to your modified copy when starting your application:

```sh
OPENSSL_CONF=/path/to/openssl.cnf python3 app.py
```

### Debug Logging

The Cloud SQL Python Connector uses the standard [Python logging module][python-logging]
Expand Down
2 changes: 1 addition & 1 deletion google/cloud/sql/connector/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async def generate_keys() -> tuple[bytes, str]:

priv_key = private_key_obj.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)

Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ async def start_proxy_server(instance: FakeCSQLInstance) -> None:
# need to be written to files in order to be loaded by the SSLContext
server_key_bytes = instance.server_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
async with AsyncTemporaryDirectory() as tmpdir:
Expand Down
9 changes: 5 additions & 4 deletions tests/unit/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric.types import CertificatePublicKeyTypes
from cryptography.x509.oid import NameOID
from google.auth import _helpers
from google.auth.credentials import Credentials
Expand Down Expand Up @@ -161,7 +162,7 @@ def self_signed_cert(cert: x509.CertificateBuilder, key: rsa.RSAPrivateKey) -> s
def client_key_signed_cert(
cert: x509.CertificateBuilder,
priv_key: rsa.RSAPrivateKey,
client_key: rsa.RSAPublicKey,
client_key: CertificatePublicKeyTypes,
cert_before: datetime.datetime | None = None,
cert_expiration: datetime.datetime | None = None,
) -> str:
Expand Down Expand Up @@ -200,7 +201,7 @@ def client_key_signed_cert(
async def create_ssl_context(instance: FakeCSQLInstance) -> ssl.SSLContext:
"""Helper method to build an ssl.SSLContext for tests"""
client_private, client_bytes = await generate_keys()
client_key: rsa.RSAPublicKey = serialization.load_pem_public_key(
client_key: CertificatePublicKeyTypes = serialization.load_pem_public_key(
client_bytes.encode("UTF-8"),
) # type: ignore
ephemeral_cert = client_key_signed_cert(
Expand Down Expand Up @@ -292,9 +293,9 @@ async def connect_settings(self, request: Any) -> web.Response:
async def generate_ephemeral(self, request: Any) -> web.Response:
body = await request.json()
pub_key = body["public_key"]
client_key: rsa.RSAPublicKey = serialization.load_pem_public_key(
client_key: CertificatePublicKeyTypes = serialization.load_pem_public_key(
pub_key.encode("UTF-8"),
)
) # type: ignore
ephemeral_cert = client_key_signed_cert(
self.server_ca,
self.server_key,
Expand Down
Loading
Loading