Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f357187
added warning
daniel-sanche Jan 9, 2026
575113c
added posargs to unit test nox command
daniel-sanche Jan 9, 2026
bef613a
added test
daniel-sanche Jan 9, 2026
15e6e7b
add rsa extra
daniel-sanche Jan 9, 2026
7715f92
updated warning
daniel-sanche Jan 9, 2026
ddacaf2
added TODO
daniel-sanche Jan 9, 2026
5573db6
added docstring warnings
daniel-sanche Jan 9, 2026
07a4f22
remove extra dependency
daniel-sanche Jan 9, 2026
6890572
Merge branch 'main' into warn_rsa
daniel-sanche Jan 9, 2026
c2ceeb4
added default rsa classes
daniel-sanche Jan 9, 2026
d1b3fb6
added shared wrapper class for RSASigner and RSAVerifier
daniel-sanche Jan 9, 2026
ad651ed
changed warning type
daniel-sanche Jan 9, 2026
540f260
added deprecation notices to docstrings
daniel-sanche Jan 9, 2026
a4830fa
Merge branch 'warn_rsa' into remove_rsa_2
daniel-sanche Jan 9, 2026
68410f1
moved warning
daniel-sanche Jan 9, 2026
f89e444
fixed warning type in tests
daniel-sanche Jan 9, 2026
fd429f3
remove rsa as required dependency
daniel-sanche Jan 9, 2026
fd1ae50
fixed errors
daniel-sanche Jan 10, 2026
0895998
added custom exception
daniel-sanche Jan 10, 2026
69bae96
ran blacken
daniel-sanche Jan 10, 2026
a803198
added new test file
daniel-sanche Jan 10, 2026
716bec3
added test file
daniel-sanche Jan 10, 2026
6191d3b
fixed bugs in implementation
daniel-sanche Jan 10, 2026
b7c270b
InvalidValue -> ValueError
daniel-sanche Jan 10, 2026
a1e0389
clean up tests
daniel-sanche Jan 10, 2026
6b17faa
finished tests
daniel-sanche Jan 10, 2026
e88fc1c
added e2e tests
daniel-sanche Jan 10, 2026
d7a3b23
added warning tests
daniel-sanche Jan 10, 2026
d0f1747
added unit tests
daniel-sanche Jan 10, 2026
db7037c
fixed lint
daniel-sanche Jan 10, 2026
556aa32
Merge branch 'main' into remove_rsa_2
daniel-sanche Jan 10, 2026
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
20 changes: 13 additions & 7 deletions google/auth/crypt/_python_rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,9 @@
_PKCS8_MARKER = ("-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----")
_PKCS8_SPEC = PrivateKeyInfo()

warnings.warn(
(
"The 'rsa' library is deprecated and will be removed in a future release. "
"Please migrate to 'cryptography'."
),
category=DeprecationWarning,
stacklevel=2,
_warning_msg = (
"The 'rsa' library is deprecated and will be removed in a future release. "
"Please migrate to 'cryptography'."
)


Expand Down Expand Up @@ -84,6 +80,11 @@ class RSAVerifier(base.Verifier):
"""

def __init__(self, public_key):
warnings.warn(
_warning_msg,
category=DeprecationWarning,
stacklevel=2,
)
self._pubkey = public_key

@_helpers.copy_docstring(base.Verifier)
Expand Down Expand Up @@ -142,6 +143,11 @@ class RSASigner(base.Signer, base.FromServiceAccountMixin):
"""

def __init__(self, private_key, key_id=None):
warnings.warn(
_warning_msg,
category=DeprecationWarning,
stacklevel=2,
)
self._key = private_key
self._key_id = key_id

Expand Down
151 changes: 142 additions & 9 deletions google/auth/crypt/rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,152 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""RSA cryptography signer and verifier."""
"""
RSA cryptography signer and verifier.

This file provides a shared wrapper, that defers to _python_rsa or _cryptography_rsa
for implmentations using different third party libraries
"""

from google.auth import _helpers
from google.auth.crypt import base
from google.auth.exceptions import MissingOptionalDependencyError

try:
# Prefer cryptograph-based RSA implementation.
# Attempt import of module that requires optional `cryptography` dependency
from google.auth.crypt import _cryptography_rsa

RSASigner = _cryptography_rsa.RSASigner
RSAVerifier = _cryptography_rsa.RSAVerifier
except ImportError: # pragma: NO COVER
# Fallback to pure-python RSA implementation if cryptography is
# unavailable.
_cryptography_rsa = None

try:
# Attempt import of module that requires optional (deprecated) `rsa` dependency
from google.auth.crypt import _python_rsa
except ImportError: # pragma: NO COVER
_python_rsa = None

RSA_NOTE = "(Note: `rsa` is also supported for legacy compatibility, but is deprecated)"


class RSAVerifier(base.Verifier):
"""Verifies RSA cryptographic signatures using public keys.

Requires installation of `cryptography` optional dependency.

.. deprecated::
The `rsa` library has been archived. Please migrate to
`cryptography` for public keys.

Args:
public_key (Union[rsa.key.PublicKey, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey]):
The public key used to verify signatures.
Raises:
ImportError: if neither `cryptograhy` or `rsa` is installed
ValueError: if an unrecognized public key is provided
Comment on lines +53 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a typo in the Raises section of the docstring. cryptograhy should be cryptography.

Suggested change
Raises:
ImportError: if neither `cryptograhy` or `rsa` is installed
ValueError: if an unrecognized public key is provided
Raises:
ImportError: if neither `cryptography` or `rsa` is installed
ValueError: if an unrecognized public key is provided

"""

def __init__(self, public_key):
module_str = public_key.__class__.__module__
if "rsa.key" in module_str:
impl_lib = _python_rsa
elif "cryptography." in module_str:
impl_lib = _cryptography_rsa
else:
raise ValueError(f"unrecognized public key type: {public_key}")
if impl_lib is None:
raise MissingOptionalDependencyError.create(self, "cryptography", RSA_NOTE)
else:
self._impl = impl_lib.RSAVerifier(public_key)

@_helpers.copy_docstring(base.Verifier)
def verify(self, message, signature):
return self._impl.verify(message, signature)

@classmethod
def from_string(cls, public_key):
"""Construct an Verifier instance from a public key or public
certificate string.

Args:
public_key (Union[str, bytes]): The public key in PEM format or the
x509 public key certificate.

Returns:
google.auth.crypt.RSAVerifier: The constructed verifier.

Raises:
ValueError: If the public_key can't be parsed.
ImportError: if neither `cryptograhy` or `rsa` is installe
"""
if _cryptography_rsa:
return _cryptography_rsa.RSAVerifier.from_string(public_key)
elif _python_rsa:
return _python_rsa.RSAVerifier.from_string(public_key)
else:
raise MissingOptionalDependencyError.create(cls, "cryptography", RSA_NOTE)
Comment on lines +76 to +96
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The from_string classmethod returns an instance of the underlying implementation class (e.g., _cryptography_rsa.RSAVerifier) instead of the wrapper class rsa.RSAVerifier. This breaks the abstraction, as callers would expect an instance of rsa.RSAVerifier.

The method should create an instance of the wrapper class and set its internal _impl to the instance created by the underlying library.

Additionally, there are a few typos in the docstring (an Verifier should be a Verifier, cryptograhy should be cryptography, and installe should be installed).

    def from_string(cls, public_key):
        """Construct a Verifier instance from a public key or public
        certificate string.

        Args:
            public_key (Union[str, bytes]): The public key in PEM format or the
                x509 public key certificate.

        Returns:
            google.auth.crypt.RSAVerifier: The constructed verifier.

        Raises:
            ValueError: If the public_key can't be parsed.
            ImportError: if neither `cryptography` or `rsa` is installed
        """
        if _cryptography_rsa:
            impl = _cryptography_rsa.RSAVerifier.from_string(public_key)
            verifier = cls.__new__(cls)
            verifier._impl = impl
            return verifier
        elif _python_rsa:
            impl = _python_rsa.RSAVerifier.from_string(public_key)
            verifier = cls.__new__(cls)
            verifier._impl = impl
            return verifier
        else:
            raise MissingOptionalDependencyError.create(cls, "cryptography", RSA_NOTE)



class RSASigner(base.Signer, base.FromServiceAccountMixin):
"""Signs messages with an RSA private key.

Requires installation of `cryptography` optional dependency.

.. deprecated::
The `rsa` library has been archived. Please migrate to
`cryptography` for public keys.

Args:
private_key (Union[rsa.key.PrivateKey, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey]):
The private key to sign with.
key_id (str): Optional key ID used to identify this private key. This
can be useful to associate the private key with its associated
public key or certificate.

Raises:
ImportError: if neither `cryptograhy` or `rsa` is installed
ValueError: if an unrecognized public key is provided
Comment on lines +115 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a typo in the Raises section of the docstring. cryptograhy should be cryptography.

    Raises:
        ImportError: if neither `cryptography` or `rsa` is installed
        ValueError: if an unrecognized public key is provided

"""

def __init__(self, private_key, key_id=None):
module_str = private_key.__class__.__module__
if "rsa.key" in module_str:
impl_lib = _python_rsa
elif "cryptography." in module_str:
impl_lib = _cryptography_rsa
else:
raise ValueError(f"unrecognized private key type: {private_key}")
if impl_lib is None:
raise MissingOptionalDependencyError.create(self, "cryptography", RSA_NOTE)
else:
self._impl = impl_lib.RSASigner(private_key, key_id=key_id)

@property # type: ignore
@_helpers.copy_docstring(base.Signer)
def key_id(self):
return self._impl.key_id

@_helpers.copy_docstring(base.Signer)
def sign(self, message):
return self._impl.sign(message)

@classmethod
def from_string(cls, key, key_id=None):
"""Construct an Signer instance from a private key in PEM format.

Args:
key (str): Private key in PEM format.
key_id (str): An optional key id used to identify the private key.

Returns:
google.auth.crypt.Signer: The constructed signer.

RSASigner = _python_rsa.RSASigner # type: ignore
RSAVerifier = _python_rsa.RSAVerifier # type: ignore
Raises:
ValueError: If the key cannot be parsed as PKCS#1 or PKCS#8 in
PEM format.
ImportError: if neither `cryptograhy` or `rsa` is installe
"""
if _cryptography_rsa:
return _cryptography_rsa.RSASigner.from_string(key, key_id=key_id)
elif _python_rsa:
return _python_rsa.RSASigner.from_string(key, key_id=key_id)
else:
raise MissingOptionalDependencyError.create(cls, "cryptography", RSA_NOTE)
Comment on lines +143 to +163
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Similar to RSAVerifier.from_string, this method returns an instance of the underlying implementation class instead of the rsa.RSASigner wrapper class. This should be corrected to return an instance of the wrapper.

Also, there are typos in the docstring (cryptograhy, installe).

    def from_string(cls, key, key_id=None):
        """Construct an Signer instance from a private key in PEM format.

        Args:
            key (str): Private key in PEM format.
            key_id (str): An optional key id used to identify the private key.

        Returns:
            google.auth.crypt.Signer: The constructed signer.

        Raises:
            ValueError: If the key cannot be parsed as PKCS#1 or PKCS#8 in
                PEM format.
            ImportError: if neither `cryptography` or `rsa` is installed
        """
        if _cryptography_rsa:
            impl = _cryptography_rsa.RSASigner.from_string(key, key_id=key_id)
            signer = cls.__new__(cls)
            signer._impl = impl
            return signer
        elif _python_rsa:
            impl = _python_rsa.RSASigner.from_string(key, key_id=key_id)
            signer = cls.__new__(cls)
            signer._impl = impl
            return signer
        else:
            raise MissingOptionalDependencyError.create(cls, "cryptography", RSA_NOTE)

15 changes: 15 additions & 0 deletions google/auth/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,18 @@ class TimeoutError(GoogleAuthError):

class ResponseError(GoogleAuthError):
"""Used to indicate an error occurred when reading an HTTP response."""


class MissingOptionalDependencyError(ImportError):
"""Raised when a user attempts to use a class that requires an optional dependency"""

@classmethod
def create(cls, caller, dependency_name, suffix_str=None):
"""Creates an instance referencing the required dependency and the triggering class"""
caller_cls = caller if isinstance(caller, type) else type(caller)
msg_str = (
f"{caller_cls.__name__} requires `{dependency_name}` optional dependency."
)
if suffix_str:
msg_str = f"{msg_str} {suffix_str}"
return cls(msg_str)
12 changes: 6 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,15 @@
from setuptools import setup


DEPENDENCIES = (
"pyasn1-modules>=0.2.1",
# rsa==4.5 is the last version to support 2.7
# https://github.com/sybrenstuvel/python-rsa/issues/152#issuecomment-643470233
"rsa>=3.1.4,<5",
)
DEPENDENCIES = ("pyasn1-modules>=0.2.1",)

cryptography_base_require = [
"cryptography >= 38.0.3",
]

# TODO: rsa is archived. Remove optional dependency in future release
rsa_extra_require = ["rsa>=3.1.4,<5"]

requests_extra_require = ["requests >= 2.20.0, < 3.0.0"]

aiohttp_extra_require = ["aiohttp >= 3.6.2, < 4.0.0", *requests_extra_require]
Expand Down Expand Up @@ -73,10 +71,12 @@
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1722): `test_aiohttp_requests` depend on
# aiohttp < 3.10.0 which is a bug. Investigate and remove the pinned aiohttp version.
"aiohttp < 3.10.0",
*rsa_extra_require,
]

extras = {
"cryptography": cryptography_base_require,
"rsa": rsa_extra_require,
"aiohttp": aiohttp_extra_require,
"enterprise_cert": enterprise_cert_extra_require,
"pyopenssl": pyopenssl_extra_require,
Expand Down
5 changes: 3 additions & 2 deletions tests/crypt/test__python_rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,9 @@ def test_from_service_account_file(self):

class TestModule(object):
def test_import_warning(self):
import importlib
from google.auth.crypt import _python_rsa

with pytest.warns(DeprecationWarning, match="The 'rsa' library is deprecated"):
importlib.reload(_python_rsa)
_python_rsa.RSAVerifier(None)
with pytest.warns(DeprecationWarning, match="The 'rsa' library is deprecated"):
_python_rsa.RSASigner(None)
Loading
Loading