diff --git a/docs/conf.py b/docs/conf.py index 8ab609390..3c95be670 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -358,6 +358,47 @@ # texinfo_no_detailmenu = False +# -- Options for autodoc -------------------------------------------------- + + +def autodoc_skip_member_handler(app, what, name, obj, skip, options): + """ + Skips members from internal modules (like _cryptography_rsa or base) + if they are publicly exposed via a higher-level package (like google.auth.crypt), + to avoid duplicate documentation entries and ambiguous cross-references. + """ + # Handle RSASigner and RSAVerifier from _cryptography_rsa + if name in ("RSASigner", "RSAVerifier") and hasattr(obj, "__module__"): + if obj.__module__ == "google.auth.crypt._cryptography_rsa": + # Check if it's also available via the public google.auth.crypt path + try: + import google.auth.crypt + + public_obj = getattr(google.auth.crypt, name, None) + if public_obj is obj: + return True # Skip this internal one + except ImportError: + pass # Should not happen if the library is installed + + # Handle Signer and Verifier from base + elif name in ("Signer", "Verifier") and hasattr(obj, "__module__"): + if obj.__module__ == "google.auth.crypt.base": + # Check if it's also available via the public google.auth.crypt path + try: + import google.auth.crypt + + public_obj = getattr(google.auth.crypt, name, None) + if public_obj is obj: + return True # Skip this internal one + except ImportError: + pass # Should not happen if the library is installed + return None # Default behavior (don't skip) + + +def setup(app): + app.connect("autodoc-skip-member", autodoc_skip_member_handler) + + # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://docs.python.org/3.5", None), diff --git a/google/auth/crypt/_python_rsa.py b/google/auth/crypt/_python_rsa.py deleted file mode 100644 index e553c25ed..000000000 --- a/google/auth/crypt/_python_rsa.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright 2016 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Pure-Python RSA cryptography implementation. - -Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages -to parse PEM files storing PKCS#1 or PKCS#8 keys as well as -certificates. There is no support for p12 files. -""" - -from __future__ import absolute_import - -import io - -from pyasn1.codec.der import decoder # type: ignore -from pyasn1_modules import pem # type: ignore -from pyasn1_modules.rfc2459 import Certificate # type: ignore -from pyasn1_modules.rfc5208 import PrivateKeyInfo # type: ignore -import rsa # type: ignore - -from google.auth import _helpers -from google.auth import exceptions -from google.auth.crypt import base - -_POW2 = (128, 64, 32, 16, 8, 4, 2, 1) -_CERTIFICATE_MARKER = b"-----BEGIN CERTIFICATE-----" -_PKCS1_MARKER = ("-----BEGIN RSA PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----") -_PKCS8_MARKER = ("-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----") -_PKCS8_SPEC = PrivateKeyInfo() - - -def _bit_list_to_bytes(bit_list): - """Converts an iterable of 1s and 0s to bytes. - - Combines the list 8 at a time, treating each group of 8 bits - as a single byte. - - Args: - bit_list (Sequence): Sequence of 1s and 0s. - - Returns: - bytes: The decoded bytes. - """ - num_bits = len(bit_list) - byte_vals = bytearray() - for start in range(0, num_bits, 8): - curr_bits = bit_list[start : start + 8] - char_val = sum(val * digit for val, digit in zip(_POW2, curr_bits)) - byte_vals.append(char_val) - return bytes(byte_vals) - - -class RSAVerifier(base.Verifier): - """Verifies RSA cryptographic signatures using public keys. - - Args: - public_key (rsa.key.PublicKey): The public key used to verify - signatures. - """ - - def __init__(self, public_key): - self._pubkey = public_key - - @_helpers.copy_docstring(base.Verifier) - def verify(self, message, signature): - message = _helpers.to_bytes(message) - try: - return rsa.pkcs1.verify(message, signature, self._pubkey) - except (ValueError, rsa.pkcs1.VerificationError): - return False - - @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._python_rsa.RSAVerifier: The constructed verifier. - - Raises: - ValueError: If the public_key can't be parsed. - """ - public_key = _helpers.to_bytes(public_key) - is_x509_cert = _CERTIFICATE_MARKER in public_key - - # If this is a certificate, extract the public key info. - if is_x509_cert: - der = rsa.pem.load_pem(public_key, "CERTIFICATE") - asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate()) - if remaining != b"": - raise exceptions.InvalidValue("Unused bytes", remaining) - - cert_info = asn1_cert["tbsCertificate"]["subjectPublicKeyInfo"] - key_bytes = _bit_list_to_bytes(cert_info["subjectPublicKey"]) - pubkey = rsa.PublicKey.load_pkcs1(key_bytes, "DER") - else: - pubkey = rsa.PublicKey.load_pkcs1(public_key, "PEM") - return cls(pubkey) - - -class RSASigner(base.Signer, base.FromServiceAccountMixin): - """Signs messages with an RSA private key. - - Args: - private_key (rsa.key.PrivateKey): 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. - """ - - def __init__(self, private_key, key_id=None): - self._key = private_key - self._key_id = key_id - - @property # type: ignore - @_helpers.copy_docstring(base.Signer) - def key_id(self): - return self._key_id - - @_helpers.copy_docstring(base.Signer) - def sign(self, message): - message = _helpers.to_bytes(message) - return rsa.pkcs1.sign(message, self._key, "SHA-256") - - @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. - - Raises: - ValueError: If the key cannot be parsed as PKCS#1 or PKCS#8 in - PEM format. - """ - key = _helpers.from_bytes(key) # PEM expects str in Python 3 - marker_id, key_bytes = pem.readPemBlocksFromFile( - io.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER - ) - - # Key is in pkcs1 format. - if marker_id == 0: - private_key = rsa.key.PrivateKey.load_pkcs1(key_bytes, format="DER") - # Key is in pkcs8. - elif marker_id == 1: - key_info, remaining = decoder.decode(key_bytes, asn1Spec=_PKCS8_SPEC) - if remaining != b"": - raise exceptions.InvalidValue("Unused bytes", remaining) - private_key_info = key_info.getComponentByName("privateKey") - private_key = rsa.key.PrivateKey.load_pkcs1( - private_key_info.asOctets(), format="DER" - ) - else: - raise exceptions.MalformedError("No key could be detected.") - - return cls(private_key, key_id=key_id) diff --git a/google/auth/crypt/rsa.py b/google/auth/crypt/rsa.py index ed842d1eb..274bafc5f 100644 --- a/google/auth/crypt/rsa.py +++ b/google/auth/crypt/rsa.py @@ -14,17 +14,7 @@ """RSA cryptography signer and verifier.""" +from google.auth.crypt import _cryptography_rsa -try: - # Prefer cryptograph-based RSA implementation. - 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. - from google.auth.crypt import _python_rsa - - RSASigner = _python_rsa.RSASigner # type: ignore - RSAVerifier = _python_rsa.RSAVerifier # type: ignore +RSASigner = _cryptography_rsa.RSASigner +RSAVerifier = _cryptography_rsa.RSAVerifier diff --git a/setup.py b/setup.py index 3874354fd..6974f8b7c 100644 --- a/setup.py +++ b/setup.py @@ -24,28 +24,22 @@ "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", -) - -# TODO(https://github.com/googleapis/google-auth-library-python/issues/1737): Unit test fails with -# `No module named 'cryptography.hazmat.backends.openssl.x509' for Python 3.7``. -cryptography_base_require = [ "cryptography >= 38.0.3", "cryptography < 39.0.0; python_version < '3.8'", -] +) requests_extra_require = ["requests >= 2.20.0, < 3.0.0"] aiohttp_extra_require = ["aiohttp >= 3.6.2, < 4.0.0", *requests_extra_require] -pyjwt_extra_require = ["pyjwt>=2.0", *cryptography_base_require] +pyjwt_extra_require = ["pyjwt>=2.0"] reauth_extra_require = ["pyu2f>=0.1.5"] # TODO(https://github.com/googleapis/google-auth-library-python/issues/1738): Add bounds for cryptography and pyopenssl dependencies. -enterprise_cert_extra_require = ["cryptography", "pyopenssl"] +enterprise_cert_extra_require = ["pyopenssl"] -pyopenssl_extra_require = ["pyopenssl>=20.0.0", cryptography_base_require] +pyopenssl_extra_require = ["pyopenssl>=20.0.0"] # TODO(https://github.com/googleapis/google-auth-library-python/issues/1739): Add bounds for urllib3 and packaging dependencies. urllib3_extra_require = ["urllib3", "packaging"] diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index c19e8785a..9bc5bbcfd 100644 Binary files a/system_tests/secrets.tar.enc and b/system_tests/secrets.tar.enc differ diff --git a/tests/crypt/test__python_rsa.py b/tests/crypt/test__python_rsa.py deleted file mode 100644 index 4a4ebe44e..000000000 --- a/tests/crypt/test__python_rsa.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright 2016 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import io -import json -import os - -import mock -from pyasn1_modules import pem # type: ignore -import pytest # type: ignore -import rsa # type: ignore - -from google.auth import _helpers -from google.auth.crypt import _python_rsa -from google.auth.crypt import base - - -DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") - -# To generate privatekey.pem, privatekey.pub, and public_cert.pem: -# $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \ -# > -keyout privatekey.pem -# $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub - -with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh: - PRIVATE_KEY_BYTES = fh.read() - PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES - -with open(os.path.join(DATA_DIR, "privatekey.pub"), "rb") as fh: - PUBLIC_KEY_BYTES = fh.read() - -with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh: - PUBLIC_CERT_BYTES = fh.read() - -# To generate pem_from_pkcs12.pem and privatekey.p12: -# $ openssl pkcs12 -export -out privatekey.p12 -inkey privatekey.pem \ -# > -in public_cert.pem -# $ openssl pkcs12 -in privatekey.p12 -nocerts -nodes \ -# > -out pem_from_pkcs12.pem - -with open(os.path.join(DATA_DIR, "pem_from_pkcs12.pem"), "rb") as fh: - PKCS8_KEY_BYTES = fh.read() - -with open(os.path.join(DATA_DIR, "privatekey.p12"), "rb") as fh: - PKCS12_KEY_BYTES = fh.read() - -# The service account JSON file can be generated from the Google Cloud Console. -SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json") - -with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh: - SERVICE_ACCOUNT_INFO = json.load(fh) - - -class TestRSAVerifier(object): - def test_verify_success(self): - to_sign = b"foo" - signer = _python_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES) - actual_signature = signer.sign(to_sign) - - verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) - assert verifier.verify(to_sign, actual_signature) - - def test_verify_unicode_success(self): - to_sign = u"foo" - signer = _python_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES) - actual_signature = signer.sign(to_sign) - - verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) - assert verifier.verify(to_sign, actual_signature) - - def test_verify_failure(self): - verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) - bad_signature1 = b"" - assert not verifier.verify(b"foo", bad_signature1) - bad_signature2 = b"a" - assert not verifier.verify(b"foo", bad_signature2) - - def test_from_string_pub_key(self): - verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) - assert isinstance(verifier, _python_rsa.RSAVerifier) - assert isinstance(verifier._pubkey, rsa.key.PublicKey) - - def test_from_string_pub_key_unicode(self): - public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES) - verifier = _python_rsa.RSAVerifier.from_string(public_key) - assert isinstance(verifier, _python_rsa.RSAVerifier) - assert isinstance(verifier._pubkey, rsa.key.PublicKey) - - def test_from_string_pub_cert(self): - verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_CERT_BYTES) - assert isinstance(verifier, _python_rsa.RSAVerifier) - assert isinstance(verifier._pubkey, rsa.key.PublicKey) - - def test_from_string_pub_cert_unicode(self): - public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES) - verifier = _python_rsa.RSAVerifier.from_string(public_cert) - assert isinstance(verifier, _python_rsa.RSAVerifier) - assert isinstance(verifier._pubkey, rsa.key.PublicKey) - - def test_from_string_pub_cert_failure(self): - cert_bytes = PUBLIC_CERT_BYTES - true_der = rsa.pem.load_pem(cert_bytes, "CERTIFICATE") - load_pem_patch = mock.patch( - "rsa.pem.load_pem", return_value=true_der + b"extra", autospec=True - ) - - with load_pem_patch as load_pem: - with pytest.raises(ValueError): - _python_rsa.RSAVerifier.from_string(cert_bytes) - load_pem.assert_called_once_with(cert_bytes, "CERTIFICATE") - - -class TestRSASigner(object): - def test_from_string_pkcs1(self): - signer = _python_rsa.RSASigner.from_string(PKCS1_KEY_BYTES) - assert isinstance(signer, _python_rsa.RSASigner) - assert isinstance(signer._key, rsa.key.PrivateKey) - - def test_from_string_pkcs1_unicode(self): - key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES) - signer = _python_rsa.RSASigner.from_string(key_bytes) - assert isinstance(signer, _python_rsa.RSASigner) - assert isinstance(signer._key, rsa.key.PrivateKey) - - def test_from_string_pkcs8(self): - signer = _python_rsa.RSASigner.from_string(PKCS8_KEY_BYTES) - assert isinstance(signer, _python_rsa.RSASigner) - assert isinstance(signer._key, rsa.key.PrivateKey) - - def test_from_string_pkcs8_extra_bytes(self): - key_bytes = PKCS8_KEY_BYTES - _, pem_bytes = pem.readPemBlocksFromFile( - io.StringIO(_helpers.from_bytes(key_bytes)), _python_rsa._PKCS8_MARKER - ) - - key_info, remaining = None, "extra" - decode_patch = mock.patch( - "pyasn1.codec.der.decoder.decode", - return_value=(key_info, remaining), - autospec=True, - ) - - with decode_patch as decode: - with pytest.raises(ValueError): - _python_rsa.RSASigner.from_string(key_bytes) - # Verify mock was called. - decode.assert_called_once_with(pem_bytes, asn1Spec=_python_rsa._PKCS8_SPEC) - - def test_from_string_pkcs8_unicode(self): - key_bytes = _helpers.from_bytes(PKCS8_KEY_BYTES) - signer = _python_rsa.RSASigner.from_string(key_bytes) - assert isinstance(signer, _python_rsa.RSASigner) - assert isinstance(signer._key, rsa.key.PrivateKey) - - def test_from_string_pkcs12(self): - with pytest.raises(ValueError): - _python_rsa.RSASigner.from_string(PKCS12_KEY_BYTES) - - def test_from_string_bogus_key(self): - key_bytes = "bogus-key" - with pytest.raises(ValueError): - _python_rsa.RSASigner.from_string(key_bytes) - - def test_from_service_account_info(self): - signer = _python_rsa.RSASigner.from_service_account_info(SERVICE_ACCOUNT_INFO) - - assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] - assert isinstance(signer._key, rsa.key.PrivateKey) - - def test_from_service_account_info_missing_key(self): - with pytest.raises(ValueError) as excinfo: - _python_rsa.RSASigner.from_service_account_info({}) - - assert excinfo.match(base._JSON_FILE_PRIVATE_KEY) - - def test_from_service_account_file(self): - signer = _python_rsa.RSASigner.from_service_account_file( - SERVICE_ACCOUNT_JSON_FILE - ) - - assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] - assert isinstance(signer._key, rsa.key.PrivateKey)