diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a9c8428f..52fd7b4f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.35.0 +current_version = 0.36.0 commit = True tag = False message = chore: Bump version from {current_version} to {new_version} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7e476a07..b72c97d3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,7 +34,7 @@ jobs: steps: - name: Check Out VCS Repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Set Up Python ${{ matrix.python_version }} uses: actions/setup-python@v5.2.0 @@ -75,7 +75,7 @@ jobs: steps: - name: Check Out VCS Repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Set Up Python ${{ matrix.python_version }} uses: actions/setup-python@v5.2.0 diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index 2bff4f41..801cda46 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -21,7 +21,7 @@ jobs: steps: - name: Check Out VCS Repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Dependency Review uses: actions/dependency-review-action@v4.3.4 diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index fe1a05af..c96e4ec4 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -38,7 +38,7 @@ jobs: steps: - name: Check Out VCS Repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Set Up Python id: set_up_python diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index cb237b21..26fd20bd 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -35,7 +35,7 @@ jobs: steps: - name: Check Out VCS Repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Set Up Python id: set_up_python diff --git a/HISTORY.md b/HISTORY.md index a59aa594..fd49e5b8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,15 @@ # History +## 0.36.0 (2024-10-03) + +- (PR #715, 2024-10-01) chore: Bump actions/checkout from 4.1.7 to 4.2.0 in production-deps group +- (PR #717, 2024-10-01) Refactor `cryptography.hazmat.*` Python imports +- (PR #716, 2024-10-02) Update cleaning regex to match RUTs with non-numeric digits +- (PR #710, 2024-10-03) chore: Bump tox from 4.20.0 to 4.21.0 in the development-dependencies group +- (PR #711, 2024-10-03) chore(deps): Bump django-filter from 24.2 to 24.3 +- (PR #714, 2024-10-03) chore(deps): Bump pytz from 2024.1 to 2024.2 +- (PR #712, 2024-10-03) chore(deps): Bump importlib-metadata from 8.4.0 to 8.5.0 + ## 0.35.0 (2024-09-26) - (PR #706, 2024-09-26) Improvements and fixes related to validation of trusted inputs diff --git a/requirements-dev.in b/requirements-dev.in index 9b49d678..88e92206 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -12,7 +12,7 @@ flake8==7.1.1 isort==5.13.2 mypy==1.11.2 pip-tools==7.4.1 -tox==4.20.0 +tox==4.21.0 twine==5.1.1 types-jsonschema==4.23.0.20240813 types-lxml==2024.9.16 diff --git a/requirements-dev.txt b/requirements-dev.txt index 61de4934..ebfed6f7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -51,7 +51,7 @@ distlib==0.3.7 # via virtualenv docutils==0.19 # via readme-renderer -filelock==3.15.4 +filelock==3.16.1 # via # tox # virtualenv @@ -59,7 +59,7 @@ flake8==7.1.1 # via -r requirements-dev.in idna==3.7 # via requests -importlib-metadata==8.4.0 +importlib-metadata==8.5.0 # via # -c requirements.txt # build @@ -97,7 +97,7 @@ pip-tools==7.4.1 # via -r requirements-dev.in pkginfo==1.8.3 # via twine -platformdirs==4.2.2 +platformdirs==4.3.6 # via # black # tox @@ -116,7 +116,7 @@ pygments==2.15.0 # via # readme-renderer # rich -pyproject-api==1.7.1 +pyproject-api==1.8.0 # via tox pyproject-hooks==1.0.0 # via @@ -155,7 +155,7 @@ tomli==2.0.1 # pyproject-api # pyproject-hooks # tox -tox==4.20.0 +tox==4.21.0 # via -r requirements-dev.in twine==5.1.1 # via -r requirements-dev.in @@ -181,12 +181,13 @@ typing-extensions==4.12.2 # black # mypy # rich + # tox # types-lxml urllib3==1.26.19 # via # requests # twine -virtualenv==20.26.3 +virtualenv==20.26.6 # via tox webencodings==0.5.1 # via bleach @@ -194,7 +195,7 @@ wheel==0.44.0 # via # -r requirements-dev.in # pip-tools -zipp==3.19.2 +zipp==3.20.2 # via # -c requirements.txt # importlib-metadata diff --git a/requirements.in b/requirements.in index 85236d9d..7518f49a 100644 --- a/requirements.in +++ b/requirements.in @@ -11,11 +11,11 @@ defusedxml==0.7.1 django-filter>=24.2 Django>=2.2.24 djangorestframework>=3.10.3,<3.16 -importlib-metadata==8.4.0 +importlib-metadata==8.5.0 jsonschema==4.23.0 lxml==5.3.0 marshmallow==3.22.0 pydantic==2.9.2 pyOpenSSL==24.2.1 -pytz==2024.1 +pytz==2024.2 signxml==3.2.2 diff --git a/requirements.txt b/requirements.txt index b32d9694..138d8f33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,11 +33,11 @@ django==4.2.15 # -r requirements.in # django-filter # djangorestframework -django-filter==24.2 +django-filter==24.3 # via -r requirements.in djangorestframework==3.15.2 # via -r requirements.in -importlib-metadata==8.4.0 +importlib-metadata==8.5.0 # via -r requirements.in importlib-resources==6.4.0 # via @@ -67,7 +67,7 @@ pyopenssl==24.2.1 # via # -r requirements.in # signxml -pytz==2024.1 +pytz==2024.2 # via -r requirements.in referencing==0.35.1 # via @@ -87,7 +87,7 @@ typing-extensions==4.12.2 # asgiref # pydantic # pydantic-core -zipp==3.19.2 +zipp==3.20.2 # via # importlib-metadata # importlib-resources diff --git a/src/cl_sii/__init__.py b/src/cl_sii/__init__.py index ee2f739d..d3e74ea3 100644 --- a/src/cl_sii/__init__.py +++ b/src/cl_sii/__init__.py @@ -4,4 +4,4 @@ """ -__version__ = '0.35.0' +__version__ = '0.36.0' diff --git a/src/cl_sii/libs/crypto_utils.py b/src/cl_sii/libs/crypto_utils.py index a82d4dd3..776cd8a4 100644 --- a/src/cl_sii/libs/crypto_utils.py +++ b/src/cl_sii/libs/crypto_utils.py @@ -46,9 +46,9 @@ import base64 from typing import Union +import cryptography.hazmat.backends.openssl.backend as _crypto_x509_backend import cryptography.x509 import signxml.util -from cryptography.hazmat.backends.openssl import backend as _crypto_x509_backend from cryptography.x509 import Certificate as X509Cert from OpenSSL.crypto import X509 as _X509CertOpenSsl diff --git a/src/cl_sii/rut/crypto_utils.py b/src/cl_sii/rut/crypto_utils.py index d8f03c8a..1f22efcc 100644 --- a/src/cl_sii/rut/crypto_utils.py +++ b/src/cl_sii/rut/crypto_utils.py @@ -2,9 +2,9 @@ from typing import Optional import cryptography +import cryptography.hazmat.backends.openssl.backend as crypto_x509_backend +import cryptography.hazmat.primitives.serialization.pkcs12 import cryptography.x509 -from cryptography.hazmat.backends.openssl import backend as crypto_x509_backend -from cryptography.hazmat.primitives.serialization import pkcs12 from . import Rut, constants @@ -22,7 +22,7 @@ def get_subject_rut_from_certificate_pfx(pfx_file_bytes: bytes, password: Option private_key, x509_cert, additional_certs, - ) = pkcs12.load_key_and_certificates( + ) = cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates( data=pfx_file_bytes, password=password.encode() if password is not None else None, backend=crypto_x509_backend, @@ -51,6 +51,14 @@ def get_subject_rut_from_certificate_pfx(pfx_file_bytes: bytes, password: Option raise Exception(f'len(results) == {len(results)}') subject_rut_raw: bytes = results[0] - subject_rut = re.sub(r'[^0-9-]', '', subject_rut_raw.decode('utf-8')) + subject_rut_str = subject_rut_raw.decode('utf-8') + + # Regex to extract Chilean RUT formatted string + rut_match = re.search(r'\b\d{1,8}-[0-9Kk]\b', subject_rut_str) + + if not rut_match: + raise Exception('RUT format not found in certificate') + + subject_rut = rut_match.group(0) return Rut(subject_rut) diff --git a/src/tests/test_data/sii-crypto/TEST-DTE-13185095-K.der b/src/tests/test_data/sii-crypto/TEST-DTE-13185095-K.der new file mode 100644 index 00000000..b279fc34 Binary files /dev/null and b/src/tests/test_data/sii-crypto/TEST-DTE-13185095-K.der differ diff --git a/src/tests/test_data/sii-crypto/TEST-DTE-13185095-K.pem b/src/tests/test_data/sii-crypto/TEST-DTE-13185095-K.pem new file mode 100644 index 00000000..cc0ebece --- /dev/null +++ b/src/tests/test_data/sii-crypto/TEST-DTE-13185095-K.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJdYOEZnh0gmB5 +hUfDzS/5oq9u0CXZ+xFozZyw+27R0frwlUioe9Xyyhzx1PyUsp3OLddI18zxLf3s +0ZP9KdoQd43P90P+oVqkQkQgt9fCabWU7SFKZEXZXAi36ubVvuOA/MgKKrcny59w +elucNoP0CbBVElLMDBIjdF6eoXykZI4LsHdU5cQ8SDGC3qmtPTl7oikZ6lGTrNXO +egoMP/rz6b6O9MJ6CdDmLCgI3zzuTlYdScXv2nNz/p424liRNXurw/5k9ouLHhb0 +j25IQV+jdz2XVvyNKSPZBINxeU8ojzrW/8y8+9lNOLUDjvA257h234YY+7nEmDy6 +JecHLQKlAgMBAAECggEABne3WTDQ/SySXFRjEW4s9B688xnLnUvqKysutJ/d1u6e +18pzIrWXEMxcUYc89KknV88w8i27bqLDXC7+SUpmrdCoxNxzWmFjv5JBDavZSWyL +X9SdFP5TH79MqFrqPkJ6m1GCOpFUf/qRi9LhzgoSAmutNY35CoP4sRqzTvRwQ/bH +4JR2mO1GD3mDvPwUpsONucujuQCpNhalgLCf2OQIG6nfHU1koJawSps8dHvqjf/g +K8x37MtE/vF+ubdyFVRkx6wv3YCaieP4lac9sOrPu7X9dtYDli8yCjJ6waILRilI +4KXL/bu+hNIw3entuB8V5V5uPP4PrQwZ43VwuabrAQKBgQDZ9Buo5B739GpL468O +ORKrHOPT1j1BOQ2Wz0V3+SbgM1CnRJ19QWIAxqLv8jRWd2o9QWm0UaEr4MpyIo2C +ZMYsIL0ALz9i39WXumwWziIiCpC5ABYt882YZX4nzhJDgt01MnVvLJzCb8J/oJdO +/un3/8maq1nVHNtdhsM9BbeNDQKBgQDsoEzgR2xlf2bA0NnAGKSoACi74NS0COAG +nxF2oq/bhPsQk8UHK5ka6otHsl0lgFRCRG+tNLWnq7jWB+ZhwoVxHeQB5ddGil6V +atQXNuW7V/Xy+CsZCe5/mekKWNdcacOo76cAqbYtLyPAkVl/381S53jEE47Us+6k +2eptxC3V+QKBgEJp0eva51zjC2jojjUlSvz9JqcsRyoSuoNT0XVHZIM438C4dczv +GW/nF0tKYIxgguz7e7xIi3YVX1r8EGbFUmWr7CucOhJk5m7/jWQ9l8ULtyHIVvnV +qrZfZtu2PXZ47/L/1yzzSSkuaPP++VxG7QB23vXUdOEtk+Kh5+g2T8IZAoGAAPij +eCQy6LO+KzpwOl6fhmUBxculc9u5d619d9wxFpiUIzxICcB/D2I5EiFESpwdPGxl +fPODb13AE3jS1EHlJFK4Fd3opUx6GOjoV/QMu1kgFFA6dQ7aYMGz+CvnLmTsvavG +JrWLnuHbprWyBVlY0WdL0po18t+OMjUGxk6Q1ZkCgYEAznH4XiQo/MHfcSkvUunm +8Hn5LI+aP+kbIg/NExrOQR3mbQaXhpAJzb4+VRX/l5XNvJ2AHv64lSKTgirvK+p7 +jmB8+pPC3XZ9nyfWYBj4+GMudVytlbDb0Sxrr9AZK3GQaVW26WBffWALAmClWFFm +bkGfTLjxBHBG6vqhFBGF/Ak= +-----END PRIVATE KEY----- diff --git a/src/tests/test_data/sii-crypto/TEST-DTE-WITH-ID-BUT-NO-RUT.der b/src/tests/test_data/sii-crypto/TEST-DTE-WITH-ID-BUT-NO-RUT.der new file mode 100644 index 00000000..60854699 Binary files /dev/null and b/src/tests/test_data/sii-crypto/TEST-DTE-WITH-ID-BUT-NO-RUT.der differ diff --git a/src/tests/test_data/sii-crypto/TEST-DTE-WITH-ID-BUT-NO-RUT.pem b/src/tests/test_data/sii-crypto/TEST-DTE-WITH-ID-BUT-NO-RUT.pem new file mode 100644 index 00000000..84fec6cc --- /dev/null +++ b/src/tests/test_data/sii-crypto/TEST-DTE-WITH-ID-BUT-NO-RUT.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCcsPQO/KxT8WNj +/pXk9japl0voyDhyN3bNqgqVgWL2jwzllR1SQRw7ja0T4uoFhWf3O3NZyGrnLV7a +K4Rj0IYrMywM6DoEuHW/R0iwO8PPK8JpENg9245GaNB45rICcm1nBBGH46ZhRa+J +VdtBwqFLvqbzbCgFseH10evfcNT2QWRvpXDdkSjejNtSDnFVXeaUNQRa8N2xgnBF +tmhVakMXsqa/RKwr320c1ksO2ukIz8x9xQPtExppSOpGbip4NqhXbEA6yAZQvyzu +UGY3GhA37rTf9ka3HYt4XTz8xokVV21Zj8IceolGUl4vbVLhCFM0aY20qEfp+4Oh +JIs/n3SFAgMBAAECggEAAoNvIsdoTz9lv/6fMmlFprJD2DPP/fsIR5PE8DF/YCOa +yhr6ea2MMaNb5aAD73s8l/Fm8AeAOX2XkinVCZHYeRxsxjc6aQV5dAxFbPhEc5AI +4g0QXuuk7Fm1kF7o95OU0Cx3SIX9Dv3iazJKnlMsKa4g+PIg8ThxfrMzlKW3cMzE +zfWdgGe65I0cOS8kLi7C+nd23y2EMn/Zfq55pguSYq0E49A+J30KzRFmZZWiXsfB +U0Q1mOHEl4Lbl0WLeJnjp+adDTgRwUMoAsuQ87SGr+8BRwZCJX2wLmdBS4NKvZFH +Xsxu4OQxxx8SySxeGKqmyE4cUAL2iCy18vS1wL/5OQKBgQDGjpoe9aU8eEt0vDbe +/SyJITMchnqOUNGEBAyRmPqViGolHd+htnrj3hJ1O44vTjUTxj3NWxon2pFYMrtj +LHBN9CBzt3/1KMjUQmQL1BmIMRlvXS8HsRQl4Nt9lP4GqtX0tbi64JydCENiavL9 +zVE0mCV4HzNSqSiTfqEVBZ/NvQKBgQDKBbalPtzQQ+sgvslMQPNpNFCpow4Nn6Eo +CHGQ9nZFFyEpdMxcpqhYwgiy8eosBXTEhbhQyYcbxB77EDH1WLcho1ZII8rX6LrM +153B/8XuIdEkJDkcYhEGmM0CAO+X8Nztkksk3T3jpG8TFdZBlk2giegBV4ko2Ina +HdkRqah6aQKBgEM6mXiOF+qHmJTn/XQ3KNMtiI7KAckaGDao4FCUCZSD4dy7ZrLs +hGOPF5TWG2htBI+zec2EYTDJUpkYZFZJ/6SFWk+T/CFYM9eauyE+KX7xkPkiBgCG +tpm0rtywi+paAaOfu/Kahqys1ZQHPkstL6etNFKdzdTZLcHzCDuD8f3JAoGATDaj +lOuGOjulNJFFN7M5IPNPiu+smY8jKQsmbN3N+HqlVBJwFnP5BqMMzRVeloTobEtW +IYQlqF/woB6X+kshq1sHbeey2ok+D5E4PrvTW+b+E3hm40JL0gVLMfpQaS3A6w9J +sfqVIpAiJz0Ru2SMnIfqMrdnUzV9q/+eqH8sxCECgYBxIN/8r/H1lmJBa8vaAO5P +Nb6j6yRgWkIIS8lN9uGjs5O3qfT0MDvkiyp6vDetHelKF+ayJ+SeNpuj978g2lhy +KgUtUYeo1qXLHHpEpmf/Dc597v+v8aI9uUD3RFdOtR2yup3JUlDVJFf/VtF27a5X +aC5sPUAoOReOMN6lLnxKrg== +-----END PRIVATE KEY----- diff --git a/src/tests/test_data/sii-crypto/howto.md b/src/tests/test_data/sii-crypto/howto.md new file mode 100644 index 00000000..219ffd63 --- /dev/null +++ b/src/tests/test_data/sii-crypto/howto.md @@ -0,0 +1,71 @@ +# Generating a self-signed certificate in DER format using OpenSSL + +## Documentation + +- + +## Parameters + +- File to send the key to (`key_file_name`) +- Output file (`certificate_file_name`) +- Number of days cert is valid for (`number_of_days`) + +```sh +key_file_name='key.pem' +certificate_file_name='certificate.der' +number_of_days=365 +subject_rut_oid='1.3.6.1.4.1.8321.1' +subject_rut='13185095-K' +``` + +## Steps + +### Generate the private key and public certificate + +```sh +openssl req \ + -newkey rsa:2048 \ + -nodes \ + -keyout "$key_file_name" \ + -x509 \ + -days "$number_of_days" \ + -outform DER \ + -out "$certificate_file_name" \ + -extensions san -config <(cat /etc/ssl/openssl.cnf \ + <(printf "\n[san]\nsubjectAltName=otherName:$subject_rut_oid;UTF8:$subject_rut")) +``` + +```text +Generating a RSA private key +....................................................................................+++++ +....................................................+++++ +writing new private key to 'key.pem' +----- +You are about to be asked to enter information that will be incorporated +into your certificate request. +What you are about to enter is what is called a Distinguished Name or a DN. +There are quite a few fields but you can leave some blank +For some fields there will be a default value, +If you enter '.', the field will be left blank. +----- +Country Name (2 letter code) [AU]:CL +State or Province Name (full name) [Some-State]:Region Metropolitana +Locality Name (eg, city) []:Santiago +Organization Name (eg, company) [Internet Widgits Pty Ltd]:Acme Corporation +Organizational Unit Name (eg, section) []:Acme Explosive Tennis Balls +Common Name (e.g. server FQDN or YOUR name) []:John Doe +Email Address []:john.doe@acme.com +``` + +### Output + +#### Review the created certificate + +```sh +openssl x509 \ + -inform DER \ + -in "$certificate_file_name" \ + -text -noout +``` + +This will generate a self-signed certificate in DER format and allow you to review its contents diff --git a/src/tests/test_rut_crypto_utils.py b/src/tests/test_rut_crypto_utils.py index 810894cc..e97287dd 100644 --- a/src/tests/test_rut_crypto_utils.py +++ b/src/tests/test_rut_crypto_utils.py @@ -1,8 +1,8 @@ import unittest from unittest.mock import Mock, patch +import cryptography.hazmat.primitives.serialization.pkcs12 import cryptography.x509 -from cryptography.hazmat.primitives.serialization import pkcs12 from cl_sii import rut from cl_sii.libs.crypto_utils import load_der_x509_cert @@ -19,7 +19,7 @@ def test_get_subject_rut_from_certificate_pfx_ok(self) -> None: x509_cert = load_der_x509_cert(cert_der_bytes) with patch.object( - pkcs12, + cryptography.hazmat.primitives.serialization.pkcs12, 'load_key_and_certificates', Mock(return_value=(None, x509_cert, None)), ): @@ -32,6 +32,46 @@ def test_get_subject_rut_from_certificate_pfx_ok(self) -> None: self.assertIsInstance(subject_rut, rut.Rut) self.assertEqual(subject_rut, rut.Rut('13185095-6')) + def test_get_subject_rut_from_certificate_pfx_ok_with_rut_that_ends_with_K(self) -> None: + cert_der_bytes = utils.read_test_file_bytes('test_data/sii-crypto/TEST-DTE-13185095-K.der') + + x509_cert = load_der_x509_cert(cert_der_bytes) + + with patch.object( + cryptography.hazmat.primitives.serialization.pkcs12, + 'load_key_and_certificates', + Mock(return_value=(None, x509_cert, None)), + ): + pfx_file_bytes = b'hello' + password = 'fake_password' + subject_rut = get_subject_rut_from_certificate_pfx( + pfx_file_bytes=pfx_file_bytes, + password=password, + ) + self.assertIsInstance(subject_rut, rut.Rut) + self.assertEqual(subject_rut, rut.Rut('13185095-K')) + + def test_get_subject_rut_from_certificate_pfx_not_matching_rut_format(self) -> None: + cert_der_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/TEST-DTE-WITH-ID-BUT-NO-RUT.der', + ) + + x509_cert = load_der_x509_cert(cert_der_bytes) + + with patch.object( + cryptography.hazmat.primitives.serialization.pkcs12, + 'load_key_and_certificates', + Mock(return_value=(None, x509_cert, None)), + ): + pfx_file_bytes = b'hello' + password = 'fake_password' + with self.assertRaises(Exception) as cm: + get_subject_rut_from_certificate_pfx( + pfx_file_bytes=pfx_file_bytes, + password=password, + ) + self.assertEqual(cm.exception.args, ('RUT format not found in certificate',)) + def test_get_subject_rut_from_certificate_pfx_fails_if_rut_info_is_missing(self) -> None: cert_der_bytes = utils.read_test_file_bytes( 'test_data/crypto/wildcard-google-com-cert.der', @@ -40,7 +80,7 @@ def test_get_subject_rut_from_certificate_pfx_fails_if_rut_info_is_missing(self) x509_cert = load_der_x509_cert(cert_der_bytes) with patch.object( - pkcs12, + cryptography.hazmat.primitives.serialization.pkcs12, 'load_key_and_certificates', Mock(return_value=(None, x509_cert, None)), ): @@ -81,7 +121,7 @@ def test_get_subject_rut_from_certificate_pfx_does_not_throw_attribute_error_if_ ) with patch.object( - pkcs12, + cryptography.hazmat.primitives.serialization.pkcs12, 'load_key_and_certificates', Mock(return_value=(None, x509_cert, None)), ), patch.object(