diff --git a/packages/google-auth/google/auth/_service_account_info.py b/packages/google-auth/google/auth/_service_account_info.py index c432080a907d..49cc283d61d5 100644 --- a/packages/google-auth/google/auth/_service_account_info.py +++ b/packages/google-auth/google/auth/_service_account_info.py @@ -52,11 +52,14 @@ def from_dict(data, require=None, use_rsa_signer=True): "fields {}.".format(", ".join(missing)) ) - # Create a signer. + # 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): 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"