From ae674cfb0fe6ac4a81a0e10085d406e281e36dba Mon Sep 17 00:00:00 2001 From: ohmayr Date: Wed, 8 Jul 2026 01:13:53 +0000 Subject: [PATCH 1/2] fix(auth): auto-detect non-RSA private keys in service account info --- .../google-auth/google/auth/_service_account_info.py | 10 +++++++++- .../google-auth/tests/test__service_account_info.py | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/google-auth/google/auth/_service_account_info.py b/packages/google-auth/google/auth/_service_account_info.py index c432080a907d..09963e6cbd5d 100644 --- a/packages/google-auth/google/auth/_service_account_info.py +++ b/packages/google-auth/google/auth/_service_account_info.py @@ -52,7 +52,15 @@ def from_dict(data, require=None, use_rsa_signer=True): "fields {}.".format(", ".join(missing)) ) - # Create a signer. + # Create a signer with auto-detection for non-RSA keys. + private_key = data.get("private_key", "") + if ( + use_rsa_signer + and isinstance(private_key, str) + and ("EC PRIVATE KEY" in private_key or "EC PARAMETERS" in private_key) + ): + use_rsa_signer = False + if use_rsa_signer: signer = crypt.RSASigner.from_service_account_info(data) else: diff --git a/packages/google-auth/tests/test__service_account_info.py b/packages/google-auth/tests/test__service_account_info.py index 7e836861e4a7..695c4725e720 100644 --- a/packages/google-auth/tests/test__service_account_info.py +++ b/packages/google-auth/tests/test__service_account_info.py @@ -63,6 +63,14 @@ def test_from_dict_es384_signer(): assert signer.algorithm == "ES384" +def test_from_dict_ec_auto_detect(): + # Pass an EC key without use_rsa_signer=False (default use_rsa_signer=True). + # It should automatically detect the EC key and return an EsSigner without error. + signer = _service_account_info.from_dict(GDCH_SERVICE_ACCOUNT_ES256_INFO) + assert isinstance(signer, crypt.EsSigner) + assert signer.key_id == GDCH_SERVICE_ACCOUNT_ES256_INFO["private_key_id"] + + def test_from_dict_bad_private_key(): info = SERVICE_ACCOUNT_INFO.copy() info["private_key"] = "garbage" From e996df44d227253c0d8aacd5ff3dc61ea78debe8 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Wed, 8 Jul 2026 09:17:35 +0000 Subject: [PATCH 2/2] fix(auth): auto-detect non-RSA private keys in service account info via try/except fallback --- .../google/auth/_service_account_info.py | 17 +++++------- .../google-auth/google/auth/crypt/__init__.py | 26 +++++++++++++++++++ .../google/auth/crypt/_cryptography_rsa.py | 3 +++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/packages/google-auth/google/auth/_service_account_info.py b/packages/google-auth/google/auth/_service_account_info.py index 09963e6cbd5d..49cc283d61d5 100644 --- a/packages/google-auth/google/auth/_service_account_info.py +++ b/packages/google-auth/google/auth/_service_account_info.py @@ -52,19 +52,14 @@ def from_dict(data, require=None, use_rsa_signer=True): "fields {}.".format(", ".join(missing)) ) - # Create a signer with auto-detection for non-RSA keys. - private_key = data.get("private_key", "") - if ( - use_rsa_signer - and isinstance(private_key, str) - and ("EC PRIVATE KEY" in private_key or "EC PARAMETERS" in private_key) - ): - use_rsa_signer = False - + # Create a signer with automatic fallback to EC and PQC (ML-DSA) signers. if use_rsa_signer: - signer = crypt.RSASigner.from_service_account_info(data) + try: + signer = crypt.RSASigner.from_service_account_info(data) + except (ValueError, TypeError): + signer = crypt.from_service_account_info(data) else: - signer = crypt.EsSigner.from_service_account_info(data) + signer = crypt.from_service_account_info(data) return signer diff --git a/packages/google-auth/google/auth/crypt/__init__.py b/packages/google-auth/google/auth/crypt/__init__.py index e56bc7b82df7..0b472520a743 100644 --- a/packages/google-auth/google/auth/crypt/__init__.py +++ b/packages/google-auth/google/auth/crypt/__init__.py @@ -56,6 +56,31 @@ RSAVerifier = rsa.RSAVerifier +def from_service_account_info(info): + """Create a Signer instance from a service account info dictionary. + + Automatically detects whether the private key is RSA or ECDSA (or other non-RSA) + and returns the appropriate Signer instance. + + Args: + info (Mapping[str, str]): Service account info dictionary. + + Returns: + google.auth.crypt.Signer: The constructed signer. + """ + private_key = info.get("private_key") + key_id = info.get("private_key_id") + if not private_key: + raise ValueError("The private_key field is missing from service account info.") + + try: + return RSASigner.from_service_account_info(info) + except (ValueError, TypeError): + pass + + return EsSigner.from_string(private_key, key_id=key_id) + + def verify_signature(message, signature, certs, verifier_cls=rsa.RSAVerifier): """Verify an RSA or ECDSA cryptographic signature. @@ -93,4 +118,5 @@ class to use for verification. This can be used to select different "RSAVerifier", "Signer", "Verifier", + "from_service_account_info", ] diff --git a/packages/google-auth/google/auth/crypt/_cryptography_rsa.py b/packages/google-auth/google/auth/crypt/_cryptography_rsa.py index 1a3e9ff52c66..39f5f05352c9 100644 --- a/packages/google-auth/google/auth/crypt/_cryptography_rsa.py +++ b/packages/google-auth/google/auth/crypt/_cryptography_rsa.py @@ -24,6 +24,7 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.asymmetric import rsa import cryptography.x509 from google.auth import _helpers @@ -133,6 +134,8 @@ def from_string(cls, key, key_id=None): private_key = serialization.load_pem_private_key( key, password=None, backend=_BACKEND ) + if not isinstance(private_key, rsa.RSAPrivateKey): + raise TypeError("Expected RSAPrivateKey, got {}".format(type(private_key))) return cls(private_key, key_id=key_id) def __getstate__(self):