diff --git a/.bumpversion.cfg b/.bumpversion.cfg index eaffc880..a9462d65 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.38.0 +current_version = 0.39.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 b72c97d3..0b87ff0d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,10 +34,10 @@ jobs: steps: - name: Check Out VCS Repository - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4.2.2 - name: Set Up Python ${{ matrix.python_version }} - uses: actions/setup-python@v5.2.0 + uses: actions/setup-python@v5.3.0 with: python-version: "${{ matrix.python_version }}" @@ -45,7 +45,7 @@ jobs: run: make python-virtualenv PYTHON_VIRTUALENV_DIR="venv" - name: Restoring/Saving Cache - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: "venv" key: py-v1-deps-${{ runner.os }}-${{ matrix.python_version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-dev.txt') }}-${{ hashFiles('Makefile', 'make/**.mk') }} @@ -75,15 +75,15 @@ jobs: steps: - name: Check Out VCS Repository - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4.2.2 - name: Set Up Python ${{ matrix.python_version }} - uses: actions/setup-python@v5.2.0 + uses: actions/setup-python@v5.3.0 with: python-version: "${{ matrix.python_version }}" - name: Restoring/Saving Cache - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: "venv" key: py-v1-deps-${{ runner.os }}-${{ matrix.python_version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-dev.txt') }}-${{ hashFiles('Makefile', 'make/**.mk') }} @@ -122,7 +122,7 @@ jobs: make test-coverage-report - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v5.0.7 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./test-reports/coverage/ @@ -137,7 +137,7 @@ jobs: - name: Store Artifacts if: ${{ always() }} - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.3 with: name: test_reports_${{ matrix.python_version }} path: test-reports/ diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index 801cda46..e453b58a 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -21,9 +21,9 @@ jobs: steps: - name: Check Out VCS Repository - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4.2.2 - name: Dependency Review - uses: actions/dependency-review-action@v4.3.4 + uses: actions/dependency-review-action@v4.5.0 with: fail-on-severity: critical diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index c96e4ec4..8875905b 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -38,16 +38,16 @@ jobs: steps: - name: Check Out VCS Repository - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4.2.2 - name: Set Up Python id: set_up_python - uses: actions/setup-python@v5.2.0 + uses: actions/setup-python@v5.3.0 with: python-version: "3.10.9" - name: Restoring/Saving Cache - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: "venv" key: py-v1-deps-${{ runner.os }}-${{ steps.set_up_python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-dev.txt') }}-${{ hashFiles('Makefile', 'make/**.mk') }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 26fd20bd..2d122a52 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -35,11 +35,11 @@ jobs: steps: - name: Check Out VCS Repository - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4.2.2 - name: Set Up Python id: set_up_python - uses: actions/setup-python@v5.2.0 + uses: actions/setup-python@v5.3.0 with: python-version: "3.10.9" @@ -47,7 +47,7 @@ jobs: run: make python-virtualenv PYTHON_VIRTUALENV_DIR="venv" - name: Restoring/Saving Cache - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: "venv" key: py-v1-deps-${{ runner.os }}-${{ steps.set_up_python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-dev.txt') }}-${{ hashFiles('Makefile', 'make/**.mk') }} @@ -68,7 +68,7 @@ jobs: make dist - name: Store Artifacts - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.3 with: name: release path: ${{ env.ARTIFACTS_PATH }}/ diff --git a/HISTORY.md b/HISTORY.md index 09c48a39..243d7015 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,17 @@ # History +## 0.39.0 (2024-12-12) + +- (PR #729, 2024-10-30) extras: Fix serialization of `None` in Pydantic `Rut` type +- (PR #730, 2024-11-19) chore: Bump the production-dependencies group with 6 updates +- (PR #733, 2024-11-20) chore: Bump the development-dependencies group across 1 directory with 5 updates +- (PR #734, 2024-12-05) chore: Bump the production-dependencies group with 2 updates +- (PR #732, 2024-12-05) chore(deps): Bump cryptography from 43.0.1 to 43.0.3 +- (PR #739, 2024-12-09) chore(deps): Bump pydantic from 2.9.2 to 2.10.3 +- (PR #740, 2024-12-09) chore(deps): Bump django from 4.2.16 to 4.2.17 +- (PR #736, 2024-12-12) chore(deps): Bump signxml from 3.2.2 to 4.0.3 +- (PR #742, 2024-12-12) deps: Update cryptography to 44.0.0 and pyopenssl to 24.3.0 + ## 0.38.0 (2024-10-28) - (PR #725, 2024-10-28) extras: Fix generation of JSON Schema for Pydantic `Rut` type diff --git a/pyproject.toml b/pyproject.toml index 66c5dad6..4512943c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ [build-system] requires = [ "setuptools==70.3.0", - "wheel==0.44.0", + "wheel==0.45.0", ] build-backend = "setuptools.build_meta" @@ -26,7 +26,7 @@ dependencies = [ "pydantic>=2.3.0,!=1.7.*,!=1.8.*,!=1.9.*", "pyOpenSSL>=22.0.0", "pytz>=2019.3", - "signxml>=3.1.0", + "signxml>=4.0.0", ] requires-python = ">=3.8, <3.11" authors = [ diff --git a/requirements-dev.in b/requirements-dev.in index 1379f1cf..0d8e5475 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -5,17 +5,17 @@ -c requirements.txt black==24.8.0 -build==1.2.2 +build==1.2.2.post1 bumpversion==0.5.3 coverage==7.6.1 flake8==7.1.1 isort==5.13.2 mypy==1.13.0 pip-tools==7.4.1 -tox==4.21.0 +tox==4.23.2 twine==5.1.1 types-jsonschema==4.23.0.20240813 -types-lxml==2024.9.16 +types-lxml==2024.11.8 types-pyOpenSSL==24.1.0.20240722 -types-pytz==2024.2.0.20240913 -wheel==0.44.0 +types-pytz==2024.2.0.20241003 +wheel==0.45.0 diff --git a/requirements-dev.txt b/requirements-dev.txt index 92ccab52..9e9b3405 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,7 +12,7 @@ black==24.8.0 # via -r requirements-dev.in bleach==5.0.1 # via readme-renderer -build==1.2.2 +build==1.2.2.post1 # via # -r requirements-dev.in # pip-tools @@ -40,7 +40,7 @@ colorama==0.4.6 # via tox coverage==7.6.1 # via -r requirements-dev.in -cryptography==43.0.1 +cryptography==44.0.0 # via # -c requirements.txt # secretstorage @@ -155,7 +155,7 @@ tomli==2.0.1 # pyproject-api # pyproject-hooks # tox -tox==4.21.0 +tox==4.23.2 # via -r requirements-dev.in twine==5.1.1 # via -r requirements-dev.in @@ -167,11 +167,11 @@ types-html5lib==1.1.11.20240806 # via types-beautifulsoup4 types-jsonschema==4.23.0.20240813 # via -r requirements-dev.in -types-lxml==2024.9.16 +types-lxml==2024.11.8 # via -r requirements-dev.in types-pyopenssl==24.1.0.20240722 # via -r requirements-dev.in -types-pytz==2024.2.0.20240913 +types-pytz==2024.2.0.20241003 # via -r requirements-dev.in types-setuptools==69.5.0.20240415 # via types-cffi @@ -191,7 +191,7 @@ virtualenv==20.26.6 # via tox webencodings==0.5.1 # via bleach -wheel==0.44.0 +wheel==0.45.0 # via # -r requirements-dev.in # pip-tools diff --git a/requirements.in b/requirements.in index 7518f49a..4a5edf65 100644 --- a/requirements.in +++ b/requirements.in @@ -6,7 +6,7 @@ # git+https://github.com/example/example.git@example-vcs-ref#egg=example-pkg[foo,bar]==1.42.3 backports-zoneinfo==0.2.1 ; python_version < "3.9" # Used by `djangorestframework`. -cryptography==43.0.1 +cryptography==44.0.0 defusedxml==0.7.1 django-filter>=24.2 Django>=2.2.24 @@ -15,7 +15,7 @@ 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 +pydantic==2.10.3 +pyOpenSSL==24.3.0 pytz==2024.2 -signxml==3.2.2 +signxml==4.0.3 diff --git a/requirements.txt b/requirements.txt index 18815cbe..b2cb5d4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,14 +21,14 @@ certifi==2024.7.4 # via signxml cffi==1.16.0 # via cryptography -cryptography==43.0.1 +cryptography==44.0.0 # via # -r requirements.in # pyopenssl # signxml defusedxml==0.7.1 # via -r requirements.in -django==4.2.16 +django==4.2.17 # via # -r requirements.in # django-filter @@ -59,14 +59,12 @@ pkgutil-resolve-name==1.3.10 # via jsonschema pycparser==2.22 # via cffi -pydantic==2.9.2 +pydantic==2.10.3 # via -r requirements.in -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic -pyopenssl==24.2.1 - # via - # -r requirements.in - # signxml +pyopenssl==24.3.0 + # via -r requirements.in pytz==2024.2 # via -r requirements.in referencing==0.35.1 @@ -77,7 +75,7 @@ rpds-py==0.19.0 # via # jsonschema # referencing -signxml==3.2.2 +signxml==4.0.3 # via -r requirements.in sqlparse==0.5.0 # via django diff --git a/src/cl_sii/__init__.py b/src/cl_sii/__init__.py index 708d3cb0..ed0d0720 100644 --- a/src/cl_sii/__init__.py +++ b/src/cl_sii/__init__.py @@ -4,4 +4,4 @@ """ -__version__ = '0.38.0' +__version__ = '0.39.0' diff --git a/src/cl_sii/extras/pydantic_types.py b/src/cl_sii/extras/pydantic_types.py index b1dd73e7..86724434 100644 --- a/src/cl_sii/extras/pydantic_types.py +++ b/src/cl_sii/extras/pydantic_types.py @@ -117,7 +117,8 @@ def validate_from_str(value: str) -> cl_sii.rut.Rut: ] ), serialization=pydantic_core.core_schema.plain_serializer_function_ser_schema( - lambda instance: instance.canonical + lambda instance: instance.canonical, + when_used='unless-none', ), ) diff --git a/src/cl_sii/libs/crypto_utils.py b/src/cl_sii/libs/crypto_utils.py index 776cd8a4..cd20233d 100644 --- a/src/cl_sii/libs/crypto_utils.py +++ b/src/cl_sii/libs/crypto_utils.py @@ -157,8 +157,7 @@ def add_pem_cert_header_footer(pem_cert: bytes) -> bytes: """ pem_value_str = pem_cert.decode('ascii') # note: it would be great if 'add_pem_header' did not forcefully convert bytes to str. - mod_pem_value_str = signxml.util.add_pem_header(pem_value_str) - mod_pem_value: bytes = mod_pem_value_str.encode('ascii') + mod_pem_value: bytes = signxml.util.add_pem_header(pem_value_str) return mod_pem_value diff --git a/src/cl_sii/libs/xml_utils.py b/src/cl_sii/libs/xml_utils.py index da4be0fd..381ebc51 100644 --- a/src/cl_sii/libs/xml_utils.py +++ b/src/cl_sii/libs/xml_utils.py @@ -440,14 +440,8 @@ def verify_xml_signature( ) if isinstance(trusted_x509_cert, crypto_utils._X509CertOpenSsl): - trusted_x509_cert_open_ssl = trusted_x509_cert - elif isinstance(trusted_x509_cert, crypto_utils.X509Cert): - trusted_x509_cert_open_ssl = crypto_utils._X509CertOpenSsl.from_cryptography( - trusted_x509_cert - ) - elif trusted_x509_cert is None: - trusted_x509_cert_open_ssl = None - else: + trusted_x509_cert = trusted_x509_cert.to_cryptography() + elif not (isinstance(trusted_x509_cert, crypto_utils.X509Cert) or trusted_x509_cert is None): # A 'crypto_utils._X509CertOpenSsl' is ok but we prefer 'crypto_utils.X509Cert'. raise TypeError("'trusted_x509_cert' must be a 'crypto_utils.X509Cert' instance, or None.") @@ -481,10 +475,10 @@ def verify_xml_signature( # https://github.com/XML-Security/signxml/commit/ef15da8dbb904f1dedfdd210ae3e0df5da535612 result = xml_verifier.verify( data=tmp_bytes, - require_x509=True, - x509_cert=trusted_x509_cert_open_ssl, - ignore_ambiguous_key_info=True, + x509_cert=trusted_x509_cert, expect_config=signxml.verifier.SignatureConfiguration( + require_x509=True, + ignore_ambiguous_key_info=True, signature_methods=frozenset([signxml.algorithms.SignatureMethod.RSA_SHA1]), digest_algorithms=frozenset([signxml.algorithms.DigestAlgorithm.SHA1]), ), diff --git a/src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml b/src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml index ded4f5c2..3abb9f87 100644 --- a/src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml +++ b/src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml @@ -70,9 +70,11 @@ -fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk -ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi -wqSxDcYjTT6vXsLPrZk= +wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD +MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4 +FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1 +GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp +0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg== @@ -87,50 +89,35 @@ Uavs/9J+gR9BBMs/eYE= -MIIIDTCCBvWgAwIBAgIQXD9eCvh/44P1ET5RI1LuJjANBgkqhkiG9w0BAQsFADBU -MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw -IwYDVQQDExxHb29nbGUgSW50ZXJuZXQgQXV0aG9yaXR5IEczMB4XDTE5MDMyNjEz -NDA0MFoXDTE5MDYxODEzMjQwMFowZjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh -bGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEzARBgNVBAoMCkdvb2ds -ZSBMTEMxFTATBgNVBAMMDCouZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49 -AwEHA0IABANpWSLXLbJm5eRzc1EJmvSIbz0nANT+b11r+XhSUCAbfQhS+4M/91YJ -gVE6UtZJrLO7GGxvp1tV/DL857NaLEWjggWSMIIFjjATBgNVHSUEDDAKBggrBgEF -BQcDATAOBgNVHQ8BAf8EBAMCB4AwggRXBgNVHREEggROMIIESoIMKi5nb29nbGUu -Y29tgg0qLmFuZHJvaWQuY29tghYqLmFwcGVuZ2luZS5nb29nbGUuY29tghIqLmNs -b3VkLmdvb2dsZS5jb22CGCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIGKi5nLmNv -gg4qLmdjcC5ndnQyLmNvbYIKKi5nZ3BodC5jboIWKi5nb29nbGUtYW5hbHl0aWNz -LmNvbYILKi5nb29nbGUuY2GCCyouZ29vZ2xlLmNsgg4qLmdvb2dsZS5jby5pboIO -Ki5nb29nbGUuY28uanCCDiouZ29vZ2xlLmNvLnVrgg8qLmdvb2dsZS5jb20uYXKC -DyouZ29vZ2xlLmNvbS5hdYIPKi5nb29nbGUuY29tLmJygg8qLmdvb2dsZS5jb20u -Y2+CDyouZ29vZ2xlLmNvbS5teIIPKi5nb29nbGUuY29tLnRygg8qLmdvb2dsZS5j -b20udm6CCyouZ29vZ2xlLmRlggsqLmdvb2dsZS5lc4ILKi5nb29nbGUuZnKCCyou -Z29vZ2xlLmh1ggsqLmdvb2dsZS5pdIILKi5nb29nbGUubmyCCyouZ29vZ2xlLnBs -ggsqLmdvb2dsZS5wdIISKi5nb29nbGVhZGFwaXMuY29tgg8qLmdvb2dsZWFwaXMu -Y26CESouZ29vZ2xlY25hcHBzLmNughQqLmdvb2dsZWNvbW1lcmNlLmNvbYIRKi5n -b29nbGV2aWRlby5jb22CDCouZ3N0YXRpYy5jboINKi5nc3RhdGljLmNvbYISKi5n -c3RhdGljY25hcHBzLmNuggoqLmd2dDEuY29tggoqLmd2dDIuY29tghQqLm1ldHJp -Yy5nc3RhdGljLmNvbYIMKi51cmNoaW4uY29tghAqLnVybC5nb29nbGUuY29tghYq -LnlvdXR1YmUtbm9jb29raWUuY29tgg0qLnlvdXR1YmUuY29tghYqLnlvdXR1YmVl -ZHVjYXRpb24uY29tghEqLnlvdXR1YmVraWRzLmNvbYIHKi55dC5iZYILKi55dGlt -Zy5jb22CGmFuZHJvaWQuY2xpZW50cy5nb29nbGUuY29tggthbmRyb2lkLmNvbYIb -ZGV2ZWxvcGVyLmFuZHJvaWQuZ29vZ2xlLmNughxkZXZlbG9wZXJzLmFuZHJvaWQu -Z29vZ2xlLmNuggRnLmNvgghnZ3BodC5jboIGZ29vLmdsghRnb29nbGUtYW5hbHl0 -aWNzLmNvbYIKZ29vZ2xlLmNvbYIPZ29vZ2xlY25hcHBzLmNughJnb29nbGVjb21t -ZXJjZS5jb22CGHNvdXJjZS5hbmRyb2lkLmdvb2dsZS5jboIKdXJjaGluLmNvbYIK -d3d3Lmdvby5nbIIIeW91dHUuYmWCC3lvdXR1YmUuY29tghR5b3V0dWJlZWR1Y2F0 -aW9uLmNvbYIPeW91dHViZWtpZHMuY29tggV5dC5iZTBoBggrBgEFBQcBAQRcMFow -LQYIKwYBBQUHMAKGIWh0dHA6Ly9wa2kuZ29vZy9nc3IyL0dUU0dJQUczLmNydDAp -BggrBgEFBQcwAYYdaHR0cDovL29jc3AucGtpLmdvb2cvR1RTR0lBRzMwHQYDVR0O -BBYEFM8C2hpNgJL/BEX/yzeB408dhba2MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgw -FoAUd8K4UJpndnaxLcKG0IOgfqZ+ukswIQYDVR0gBBowGDAMBgorBgEEAdZ5AgUD -MAgGBmeBDAECAjAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vY3JsLnBraS5nb29n -L0dUU0dJQUczLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAF9PM41ShwCbhtJG7tj2y -ZvF2sHbQ5YuZrMfJc6eeCG+nCKm1U5iJzXnXctFGvfJnUCZpj9YrfwDswdEddWyZ -IG6m6wONF3ZiQifQrcDi0oDA+0BwjEuzYGCGkbfE+Xxb30bVEyDRe51DpJf+cqsb -+DW2pYdikbdrPem5/hwdNerc7nqrQOJ93sqwbVNGktuyJsTOGNKkSwSaejxdN7yl -g5aa4CJsE94gy4+mCywWjnnsjcLGJM3RBUxDdAdTGMldU/r33HCUCXl33Qxc4nvP -MlE9LyFOTIJoajWcpGOsbKWiL3Zr19DKNBSn4Xof0onbtCH7dbpyMwP8XcA2O1dA -ow== +MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx +HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE +ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD +EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW +GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa +MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh +MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE +CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM +b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI +hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx +O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC +/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3 +FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV +HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S +ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu +Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6 +Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1 +MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC +AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu +Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp +AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A +IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk +AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA +cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA +mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD +qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un ++/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud +B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY ++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw== diff --git a/src/tests/test_extras_pydantic_types.py b/src/tests/test_extras_pydantic_types.py index 3664e122..7c07da73 100644 --- a/src/tests/test_extras_pydantic_types.py +++ b/src/tests/test_extras_pydantic_types.py @@ -49,6 +49,19 @@ def test_serialize_to_python(self) -> None: self.assertEqual(expected_serialized_value, actual_serialized_value) + def test_serialize_none_to_python(self) -> None: + # -----Arrange----- + + instance = None + + # -----Act----- + + actual_serialized_value = self.pydantic_type_adapter.dump_python(instance) + + # -----Assert----- + + self.assertIsNone(actual_serialized_value) + def test_serialize_to_json(self) -> None: # -----Arrange----- @@ -67,6 +80,24 @@ def test_serialize_to_json(self) -> None: self.assertEqual(expected_serialized_value, actual_serialized_value) + def test_serialize_none_to_json(self) -> None: + # -----Arrange----- + + instance = None + + expected_serialized_value = b'null' + self.assertEqual( + expected_serialized_value, json.dumps(json.loads(expected_serialized_value)).encode() + ) + + # -----Act----- + + actual_serialized_value = self.pydantic_type_adapter.dump_json(instance) + + # -----Assert----- + + self.assertEqual(expected_serialized_value, actual_serialized_value) + def test_deserialize_from_instance(self) -> None: # -----Arrange----- diff --git a/src/tests/test_libs_xml_utils.py b/src/tests/test_libs_xml_utils.py index e30c7f45..a503f2d5 100644 --- a/src/tests/test_libs_xml_utils.py +++ b/src/tests/test_libs_xml_utils.py @@ -1,9 +1,15 @@ +import datetime import io import unittest +from typing import Any +from unittest import mock import lxml.etree +import signxml -from cl_sii.libs.crypto_utils import load_pem_x509_cert +from cl_sii.base.constants import SII_OFFICIAL_TZ +from cl_sii.libs.crypto_utils import _X509CertOpenSsl, load_pem_x509_cert +from cl_sii.libs.tz_utils import convert_naive_dt_to_tz_aware from cl_sii.libs.xml_utils import ( # noqa: F401 XmlElement, XmlFeatureForbidden, @@ -185,6 +191,28 @@ def test_ok_external_trusted_cert(self) -> None: signature_xml_bytes = f.getvalue() self.assertEqual(signature_xml_bytes, self.with_valid_signature_signature_xml) + def test_ok_external_trusted_open_ssl_cert_with_signature(self) -> None: + xml_doc = parse_untrusted_xml(self.with_valid_signature) + cert = load_pem_x509_cert(self.xml_doc_cert_pem_bytes) + + open_ssl_cert = _X509CertOpenSsl.from_cryptography(cert) + + signed_data, signed_xml, signature_xml = verify_xml_signature( + xml_doc, trusted_x509_cert=open_ssl_cert + ) + + self.assertEqual(signed_data, self.with_valid_signature_signed_data) + + f = io.BytesIO() + write_xml_doc(signed_xml, f) + signed_xml_bytes = f.getvalue() + self.assertEqual(signed_xml_bytes, self.with_valid_signature_signed_xml) + + f = io.BytesIO() + write_xml_doc(signature_xml, f) + signature_xml_bytes = f.getvalue() + self.assertEqual(signature_xml_bytes, self.with_valid_signature_signature_xml) + def test_ok_cert_in_signature(self) -> None: # TODO: implement! @@ -221,7 +249,7 @@ def test_fail_verify_with_other_cert(self) -> None: verify_xml_signature(xml_doc, trusted_x509_cert=cert) self.assertEqual( cm.exception.args, - ("Signature verification failed: wrong signature length",), + ("Signature verification failed: ",), ) def test_bad_cert_included(self) -> None: @@ -244,26 +272,58 @@ def test_bad_cert_included(self) -> None: ) def test_fail_replaced_cert(self) -> None: + """ + Tests that the signature verification fails + when the certificate is not the one that was used to sign the document. + """ xml_doc = parse_untrusted_xml(self.with_replaced_cert) - cert = load_pem_x509_cert(self.any_x509_cert_pem_file) + cert = load_pem_x509_cert(self.xml_doc_cert_pem_bytes) with self.assertRaises(XmlSignatureInvalid) as cm: verify_xml_signature(xml_doc, trusted_x509_cert=cert) self.assertEqual( cm.exception.args, - ("Signature verification failed: []",), + ("Signature verification failed: ",), ) def test_fail_included_cert_not_from_a_known_ca(self) -> None: xml_doc = parse_untrusted_xml(self.with_valid_signature) + xml_doc_signature_timestamp = convert_naive_dt_to_tz_aware( + dt=datetime.datetime.fromisoformat('2019-04-01T01:36:40'), # From XML doc’s + tz=SII_OFFICIAL_TZ, + ) + + def _get_cert_chain_verifier( + *args: Any, **kwargs: Any + ) -> signxml.util.X509CertChainVerifier: + # The default signature verification time is the current time (see + # https://cryptography.io/en/43.0.3/x509/verification/#cryptography.x509.verification.PolicyBuilder.time + # ), but that causes verification to fail with the message + # “validation failed: cert is not valid at validation time”. + # To avoid that, we set the verification time to the time of the signature. + return signxml.util.X509CertChainVerifier( + ca_pem_file=kwargs['ca_pem_file'], verification_time=xml_doc_signature_timestamp + ) # Without cert: fails because the issuer of the cert in the signature is not a known CA. - with self.assertRaises(XmlSignatureInvalidCertificate) as cm: + with self.assertRaises(XmlSignatureInvalidCertificate) as cm, mock.patch.object( + signxml.verifier.XMLVerifier, + 'get_cert_chain_verifier', + side_effect=_get_cert_chain_verifier, + ) as mock_get_cert_chain_verifier: verify_xml_signature(xml_doc, trusted_x509_cert=None) self.assertEqual( cm.exception.args, - ('unable to get local issuer certificate',), + # According to some test cases from https://x509-limbo.com/, OpenSSL’s error message + # “unable to get local issuer certificate” seems to be equivalent to PyCA Cryptography’s + # error message below: + ( + 'validation failed:' + ' candidates exhausted:' + ' all candidates exhausted with no interior errors', + ), ) + mock_get_cert_chain_verifier.assert_called_once_with(ca_pem_file=None) def test_fail_signed_data_modified(self) -> None: xml_doc = parse_untrusted_xml(self.with_signature_and_modified)