diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index e3aee77a..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,27 +0,0 @@ -environment: - matrix: - - python: 27 - - python: 27-x64 - - python: 35 - - python: 35-x64 - - python: 36 - - python: 36-x64 - - python: 37 - - python: 37-x64 - - python: 38 - - python: 38-x64 - -install: - - SET PATH=C:\\Python%PYTHON%;c:\\Python%PYTHON%\\scripts;%PATH% - - python -m pip install -U pip wheel setuptools - -build: off -build_script: - - python setup.py bdist_wheel - -test: off -test_script: - - pip install -r requirements-test.txt - - pip install xmlsec --only-binary=xmlsec --no-index --find-links=dist - - pytest -v --color=yes - - ps: Get-ChildItem dist\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/.github/workflows/linuxbrew.yml b/.github/workflows/linuxbrew.yml new file mode 100644 index 00000000..1c6d9543 --- /dev/null +++ b/.github/workflows/linuxbrew.yml @@ -0,0 +1,42 @@ +name: linuxbrew +on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'master' }} +jobs: + linuxbrew: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + env: + # For some unknown reason, linuxbrew tries to use "gcc-11" by default, which doesn't exist. + CC: gcc + steps: + - uses: actions/checkout@v3 + - name: Install brew + run: | + sudo apt install -y build-essential procps curl file git + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH + - name: Install build dependencies + run: | + brew update + brew install python@${{ matrix.python }} gcc libxml2 libxmlsec1 pkg-config + echo "/home/linuxbrew/.linuxbrew/opt/python@${{ matrix.python }}/libexec/bin" >> $GITHUB_PATH + - name: Build wheel + run: | + python3 -m venv build_venv + source build_venv/bin/activate + pip3 install --upgrade setuptools wheel build + export CFLAGS="-I$(brew --prefix)/include" + export LDFLAGS="-L$(brew --prefix)/lib" + python3 -m build + rm -rf build/ + - name: Run tests + run: | + python3 -m venv test_venv + source test_venv/bin/activate + pip3 install --upgrade --no-binary=lxml -r requirements-test.txt + pip3 install xmlsec --only-binary=xmlsec --no-index --find-links=dist/ + pytest -v --color=yes diff --git a/.github/workflows/macosx.yml b/.github/workflows/macosx.yml index daf2c3dd..e2e2a0df 100644 --- a/.github/workflows/macosx.yml +++ b/.github/workflows/macosx.yml @@ -1,28 +1,54 @@ -name: MacOS +name: macOS on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'master' }} jobs: macosx: runs-on: macos-latest strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + static_deps: ["static", ""] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} - name: Install build dependencies run: | - pip install --upgrade pip setuptools wheel + pip install --upgrade pip setuptools wheel build brew install libxml2 libxmlsec1 pkg-config + - name: Build macosx_x86_64 wheel + env: + CC: clang + CFLAGS: "-fprofile-instr-generate -fcoverage-mapping" + LDFLAGS: "-fprofile-instr-generate -fcoverage-mapping" + PYXMLSEC_STATIC_DEPS: ${{ matrix.static_deps }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + export PKG_CONFIG_PATH="$(brew --prefix)/opt/libxml2/lib/pkgconfig" + export PYXMLSEC_LIBXML2_VERSION="$(pkg-config --modversion libxml-2.0)" + python -m build + rm -rf build/ - name: Set environment variables shell: bash run: | - echo ::set-env name=PKGVER::$(python setup.py --version) - - name: Build macosx_x86_64 wheel - run: | - python setup.py bdist_wheel + echo "PKGVER=$(python setup.py --version)" >> $GITHUB_ENV + echo "LLVM_PROFILE_FILE=pyxmlsec.profraw" >> $GITHUB_ENV - name: Install test dependencies run: | - pip install --upgrade -r requirements-test.txt + export PKG_CONFIG_PATH="$(brew --prefix)/opt/libxml2/lib/pkgconfig" + pip install coverage --upgrade --no-binary=lxml -r requirements-test.txt pip install xmlsec --only-binary=xmlsec --no-index --find-links=dist/ + echo "PYXMLSEC_LIBFILE=$(python -c 'import xmlsec; print(xmlsec.__file__)')" >> $GITHUB_ENV - name: Run tests run: | - pytest -v --color=yes + coverage run -m pytest -v --color=yes + - name: Report coverage to codecov + run: | + /Library/Developer/CommandLineTools/usr/bin/llvm-profdata merge -sparse ${{ env.LLVM_PROFILE_FILE }} -output pyxmlsec.profdata + /Library/Developer/CommandLineTools/usr/bin/llvm-cov show ${{ env.PYXMLSEC_LIBFILE }} --arch=$(uname -m) --instr-profile=pyxmlsec.profdata src > coverage.txt + bash <(curl -s https://codecov.io/bash) -f coverage.txt + if: matrix.static_deps != 'static' diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml new file mode 100644 index 00000000..d1c205d7 --- /dev/null +++ b/.github/workflows/manylinux.yml @@ -0,0 +1,52 @@ +name: manylinux +on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'master' }} +jobs: + manylinux: + runs-on: ubuntu-latest + strategy: + matrix: + python-abi: [cp39-cp39, cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313, cp314-cp314] + image: + - manylinux2014_x86_64 + - manylinux_2_28_x86_64 + - musllinux_1_2_x86_64 + container: quay.io/pypa/${{ matrix.image }} + steps: + - uses: actions/checkout@v1 + - name: Install python build dependencies + run: | + # https://github.com/actions/runner/issues/2033 + chown -R $(id -u):$(id -g) $PWD + /opt/python/${{ matrix.python-abi }}/bin/pip install --upgrade pip setuptools wheel build + - name: Install system build dependencies (manylinux) + run: | + yum install -y perl-core + if: contains(matrix.image, 'manylinux') + - name: Set environment variables + shell: bash + run: | + echo "PKGVER=$(/opt/python/${{ matrix.python-abi }}/bin/python setup.py --version)" >> $GITHUB_ENV + - name: Build linux_x86_64 wheel + env: + PYXMLSEC_STATIC_DEPS: true + PYXMLSEC_LIBXML2_VERSION: 2.14.6 # Lock it to libxml2 2.14.6 to match it with lxml + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + /opt/python/${{ matrix.python-abi }}/bin/python -m build + - name: Label manylinux wheel + run: | + ls -la dist/ + auditwheel show dist/xmlsec-${{ env.PKGVER }}-${{ matrix.python-abi }}-linux_x86_64.whl + auditwheel repair dist/xmlsec-${{ env.PKGVER }}-${{ matrix.python-abi }}-linux_x86_64.whl + ls -la wheelhouse/ + auditwheel show wheelhouse/xmlsec-${{ env.PKGVER }}-${{ matrix.python-abi }}-*${{ matrix.image }}*.whl + - name: Install test dependencies + run: | + /opt/python/${{ matrix.python-abi }}/bin/pip install --upgrade -r requirements-test.txt + /opt/python/${{ matrix.python-abi }}/bin/pip install xmlsec --only-binary=xmlsec --no-index --find-links=wheelhouse/ + - name: Run tests + run: | + /opt/python/${{ matrix.python-abi }}/bin/pytest -v --color=yes diff --git a/.github/workflows/manylinux2010.yml b/.github/workflows/manylinux2010.yml deleted file mode 100644 index 7d58279a..00000000 --- a/.github/workflows/manylinux2010.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: manylinux2010 -on: [push, pull_request] -jobs: - manylinux2010_x86_64: - runs-on: ubuntu-latest - container: quay.io/pypa/manylinux2010_x86_64 - strategy: - matrix: - python-abi: [cp27-cp27m, cp27-cp27mu, cp35-cp35m, cp36-cp36m, cp37-cp37m, cp38-cp38] - steps: - - uses: actions/checkout@v1 - - name: Install build dependencies - run: | - /opt/python/${{ matrix.python-abi }}/bin/pip install --upgrade pip setuptools wheel - - name: Set environment variables - shell: bash - run: | - echo ::set-env name=PKGVER::$(/opt/python/${{ matrix.python-abi }}/bin/python setup.py --version) - - name: Build linux_x86_64 wheel - env: - STATIC_DEPS: true - run: | - /opt/python/${{ matrix.python-abi }}/bin/python setup.py bdist_wheel - - name: Label manylinux2010_x86_64 wheel - run: | - ls -la dist/ - auditwheel show dist/xmlsec-${PKGVER}-${{ matrix.python-abi }}-linux_x86_64.whl - auditwheel repair dist/xmlsec-${PKGVER}-${{ matrix.python-abi }}-linux_x86_64.whl - ls -l wheelhouse/ - auditwheel show wheelhouse/xmlsec-${PKGVER}-${{ matrix.python-abi }}-manylinux2010_x86_64.whl - - name: Install test dependencies - run: | - /opt/python/${{ matrix.python-abi }}/bin/pip install --upgrade -r requirements-test.txt - /opt/python/${{ matrix.python-abi }}/bin/pip install xmlsec --only-binary=xmlsec --no-index --find-links=wheelhouse/ - - name: Run tests - run: | - /opt/python/${{ matrix.python-abi }}/bin/pytest -v --color=yes diff --git a/.github/workflows/sdist.yml b/.github/workflows/sdist.yml index 9c6e1282..ecc53c31 100644 --- a/.github/workflows/sdist.yml +++ b/.github/workflows/sdist.yml @@ -1,14 +1,24 @@ name: sdist on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'master' }} jobs: sdist: - runs-on: ubuntu-latest + # Avoid Ubuntu 24.04 in sdist workflows, because it contains libxmlsec1-dev + # v1.2.39, which has a bug that causes tests/test_pkcs11.py to fail. + # (It thinks the softhsm engine has a public key instead of a private key.) + # libxmlsec1 <=1.2.33 or >=1.2.42 works. TODO: Try 26.04 when available. + runs-on: ubuntu-22.04 + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.8 - uses: actions/setup-python@v1 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: ${{ matrix.python }} - name: Install build dependencies run: | pip install --upgrade pip setuptools wheel @@ -16,10 +26,10 @@ jobs: run: | python setup.py sdist - name: Install test dependencies - env: - STATIC_DEPS: true run: | - pip install --upgrade -r requirements-test.txt + sudo apt-get update + sudo apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl opensc softhsm2 libengine-pkcs11-openssl + pip install --upgrade -r requirements-test.txt --no-binary lxml pip install dist/xmlsec-$(python setup.py --version).tar.gz - name: Run tests run: | diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 00000000..1d4564a6 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,138 @@ +name: Wheel build + +on: + release: + types: [created] + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # │ │ │ │ │ + - cron: "42 3 * * 4" + push: + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'master' }} + +permissions: {} + +jobs: + sdist: + # Avoid Ubuntu 24.04 in sdist workflows, because it contains libxmlsec1-dev + # v1.2.39, which has a bug that causes tests/test_pkcs11.py to fail. + # (It thinks the softhsm engine has a public key instead of a private key.) + # libxmlsec1 <=1.2.33 or >=1.2.42 works. TODO: Try 26.04 when available. + runs-on: ubuntu-22.04 + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5.0.0 + with: + python-version: "3.x" + + - name: Install build dependencies + run: | + pip install --upgrade pip setuptools wheel + + - name: Package source dist + run: python setup.py sdist + + - name: Install test dependencies + run: | + sudo apt-get update -y -q + sudo apt-get install -y -q libxml2-dev libxslt1-dev libxmlsec1-dev libxmlsec1-openssl opensc softhsm2 libengine-pkcs11-openssl + pip install --upgrade -r requirements-test.txt --no-binary lxml + pip install dist/xmlsec-$(python setup.py --version).tar.gz + + - name: Run tests + run: pytest -v --color=yes + + - name: Upload sdist + uses: actions/upload-artifact@v4.3.1 + with: + name: sdist + path: dist/*.tar.gz + + generate-wheels-matrix: + # Create a matrix of all architectures & versions to build. + # This enables the next step to run cibuildwheel in parallel. + # From https://iscinumpy.dev/post/cibuildwheel-2-10-0/#only-210 + name: Generate wheels matrix + runs-on: ubuntu-latest + outputs: + include: ${{ steps.set-matrix.outputs.include }} + steps: + - uses: actions/checkout@v4 + - name: Install cibuildwheel + # Nb. keep cibuildwheel version pin consistent with job below + run: pipx install cibuildwheel==3.1.4 + - id: set-matrix + # Once we have the windows build figured out, it can be added here + # by updating the matrix to include windows builds as well. + # See example here: + # https://github.com/lxml/lxml/blob/3ccc7d583e325ceb0ebdf8fc295bbb7fc8cd404d/.github/workflows/wheels.yml#L95C1-L106C51 + run: | + MATRIX=$( + { + cibuildwheel --print-build-identifiers --platform linux \ + | jq -nRc '{"only": inputs, "os": "ubuntu-latest"}' \ + && cibuildwheel --print-build-identifiers --platform macos \ + | jq -nRc '{"only": inputs, "os": "macos-latest"}' \ + && cibuildwheel --print-build-identifiers --platform windows \ + | jq -nRc '{"only": inputs, "os": "windows-2022"}' \ + && cibuildwheel --print-build-identifiers --platform windows --archs ARM64 \ + | jq -nRc '{"only": inputs, "os": "windows-11-arm"}' + } | jq -sc + ) + echo "include=$MATRIX" + echo "include=$MATRIX" >> $GITHUB_OUTPUT + + build_wheels: + name: Build for ${{ matrix.only }} + needs: generate-wheels-matrix + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.generate-wheels-matrix.outputs.include) }} + + env: + PYXMLSEC_LIBXML2_VERSION: 2.14.6 + PYXMLSEC_LIBXSLT_VERSION: 1.1.43 + + steps: + - name: Check out the repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Build wheels + uses: pypa/cibuildwheel@v3.1.4 + with: + only: ${{ matrix.only }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/upload-artifact@v4.3.1 + with: + path: ./wheelhouse/*.whl + name: xmlsec-wheel-${{ matrix.only }} diff --git a/.gitignore b/.gitignore index 1ef359b3..15f47985 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,8 @@ !.travis* !.appveyor* !.git* -!.readthedocs.yml +!.readthedocs.yaml +!.pre-commit-config.yaml # Python /dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..aca65390 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.4 + hooks: + - id: ruff + args: ["--fix"] + types: [python] + - id: ruff-format + types: [python] + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: no-commit-to-branch + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-ast + - id: check-merge-conflict + - id: check-json + - id: detect-private-key + exclude: ^.*/rsakey.pem$ + - id: mixed-line-ending + - id: pretty-format-json + args: [--autofix] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.18.2 + hooks: + - id: mypy + exclude: (setup.py|tests|build_support/.*.py|doc/.*) + types: [] + files: ^.*.pyi?$ + additional_dependencies: [lxml-stubs, types-docutils] + +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: rst-backticks diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 74% rename from .readthedocs.yml rename to .readthedocs.yaml index 4d8647be..93665c84 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yaml @@ -1,10 +1,14 @@ version: 2 +build: + os: ubuntu-20.04 + tools: + python: '3.9' + sphinx: configuration: doc/source/conf.py python: - version: 3.7 install: - method: pip path: . diff --git a/.travis.yml b/.travis.yml index b5738884..8d3ca07e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,49 +1,53 @@ -dist: trusty -sudo: false +dist: focal language: python +travis: + auto_cancel: + push: true + pull_request: true + notifications: email: false -matrix: - include: - - python: 2.7 - - python: 3.5 - - python: 3.6 - - python: 3.7 - dist: xenial - sudo: required - - python: 3.8 - dist: xenial - sudo: required - - python: 3.9-dev - dist: xenial - sudo: required + +python: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + env: global: - - CFLAGS=-coverage - - LDFLAGS=-coverage -lgcov - - PYXMLSEC_TEST_ITERATIONS=50 + - CFLAGS=-coverage + - LDFLAGS=-coverage -lgcov + - PYXMLSEC_TEST_ITERATIONS=50 addons: apt: packages: - - libssl-dev - - libxmlsec1 - - libxmlsec1-dev - - libxmlsec1-openssl - - libxslt1-dev - - pkg-config - - lcov + - libssl-dev + - libxmlsec1 + - libxmlsec1-dev + - libxmlsec1-openssl + - libxslt1-dev + - pkg-config + - lcov + install: -- travis_retry pip install --upgrade pip setuptools wheel -- travis_retry pip install coverage codecov -r requirements-test.txt --upgrade --force-reinstall -- travis_retry pip install -e "." -- pip list -script: coverage run -m pytest -v tests --color=yes + - travis_retry pip install --upgrade pip setuptools wheel + - travis_retry pip install coverage -r requirements-test.txt --upgrade --force-reinstall + - python setup.py bdist_wheel + - pip install xmlsec --only-binary=xmlsec --no-index --find-links=dist/ + +script: + - coverage run -m pytest -v tests --color=yes + after_success: -- lcov --capture --directory . --output-file coverage.info -- lcov --list coverage.info -- codecov --file coverage.info + - lcov --capture --no-external --directory . --output-file coverage.info + - lcov --list coverage.info + - bash <(curl -s https://codecov.io/bash) -f coverage.info + before_deploy: -- travis_retry pip install Sphinx -r doc/source/requirements.txt -- git apply --verbose --no-index --unsafe-paths --directory=$(python -c "import site; print(site.getsitepackages()[0])") doc/source/sphinx-pr-6916.diff -- sphinx-build -EWanb html doc/source build/sphinx + - travis_retry pip install Sphinx -r doc/source/requirements.txt + - git apply --verbose --no-index --unsafe-paths --directory=$(python -c "import site; print(site.getsitepackages()[0])") doc/source/sphinx-pr-6916.diff + - sphinx-build -EWanb html doc/source build/sphinx diff --git a/README.md b/README.md new file mode 100644 index 00000000..60bde880 --- /dev/null +++ b/README.md @@ -0,0 +1,198 @@ +# python-xmlsec + +[![image](https://img.shields.io/pypi/v/xmlsec.svg?logo=python&logoColor=white)](https://pypi.python.org/pypi/xmlsec) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/xmlsec/python-xmlsec/master.svg)](https://results.pre-commit.ci/latest/github/xmlsec/python-xmlsec/master) +[![image](https://github.com/xmlsec/python-xmlsec/actions/workflows/manylinux.yml/badge.svg)](https://github.com/xmlsec/python-xmlsec/actions/workflows/manylinux.yml) +[![image](https://github.com/xmlsec/python-xmlsec/actions/workflows/macosx.yml/badge.svg)](https://github.com/xmlsec/python-xmlsec/actions/workflows/macosx.yml) +[![image](https://github.com/xmlsec/python-xmlsec/actions/workflows/linuxbrew.yml/badge.svg)](https://github.com/xmlsec/python-xmlsec/actions/workflows/linuxbrew.yml) +[![image](https://codecov.io/gh/xmlsec/python-xmlsec/branch/master/graph/badge.svg)](https://codecov.io/gh/xmlsec/python-xmlsec) +[![Documentation Status](https://img.shields.io/readthedocs/xmlsec/latest?logo=read-the-docs)](https://xmlsec.readthedocs.io/en/latest/?badge=latest) + +Python bindings for the [XML Security +Library](https://www.aleksey.com/xmlsec/). + +## Documentation + +Documentation for `xmlsec` can be found at +[xmlsec.readthedocs.io](https://xmlsec.readthedocs.io/). + +## Usage + +Check the +[examples](https://xmlsec.readthedocs.io/en/latest/examples.html) +section in the documentation to see various examples of signing and +verifying using the library. + +## Requirements + +- `libxml2 >= 2.9.1` +- `libxmlsec1 >= 1.2.33` + +## Install + +`xmlsec` is available on PyPI: + +``` bash +pip install xmlsec +``` + +Depending on your OS, you may need to install the required native +libraries first: + +### Linux (Debian) + +``` bash +apt-get install pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl +``` + +Note: There is no required version of LibXML2 for Ubuntu Precise, so you +need to download and install it manually. + +``` bash +wget http://xmlsoft.org/sources/libxml2-2.9.1.tar.gz +tar -xvf libxml2-2.9.1.tar.gz +cd libxml2-2.9.1 +./configure && make && make install +``` + +### Linux (CentOS) + +``` bash +yum install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel +``` + +### Linux (Fedora) + +``` bash +dnf install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel +``` + +### Mac + +``` bash +brew install libxml2 libxmlsec1 pkg-config +``` + +or + +``` bash +port install libxml2 xmlsec pkgconfig +``` + +### Alpine + +``` bash +apk add build-base openssl libffi-dev openssl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec +``` + +## Troubleshooting + +### Mac + +If you get any fatal errors about missing `.h` files, update your +`C_INCLUDE_PATH` environment variable to include the appropriate files +from the `libxml2` and `libxmlsec1` libraries. + +### Windows + +Starting with 1.3.7, prebuilt wheels are available for Windows, so +running `pip install xmlsec` should suffice. If you want to build from +source: + +1. Configure build environment, see + [wiki.python.org](https://wiki.python.org/moin/WindowsCompilers) for + more details. + +2. Install from source dist: + + ``` bash + pip install xmlsec --no-binary=xmlsec + ``` + +## Building from source + +1. Clone the `xmlsec` source code repository to your local computer. + + ``` bash + git clone https://github.com/xmlsec/python-xmlsec.git + ``` + +2. Change into the `python-xmlsec` root directory. + + ``` bash + cd /path/to/xmlsec + ``` + +3. Install the project and all its dependencies using `pip`. + + ``` bash + pip install . + ``` + +## Contributing + +### Setting up your environment + +1. Follow steps 1 and 2 of the [manual installation + instructions](#building-from-source). + +2. Initialize a virtual environment to develop in. This is done so as + to ensure every contributor is working with close-to-identical + versions of packages. + + ``` bash + mkvirtualenv xmlsec + ``` + + The `mkvirtualenv` command is available from `virtualenvwrapper` + package which can be installed by following + [link](http://virtualenvwrapper.readthedocs.org/en/latest/install.html#basic-installation). + +3. Activate the created virtual environment: + + ``` bash + workon xmlsec + ``` + +4. Install `xmlsec` in development mode with testing enabled. This will + download all dependencies required for running the unit tests. + + ``` bash + pip install -r requirements-test.txt + pip install -e "." + ``` + +### Running the test suite + +1. [Set up your environment](#setting-up-your-environment). + +2. Run the unit tests. + + ``` bash + pytest tests + ``` + +3. Tests configuration + + Env variable `PYXMLSEC_TEST_ITERATIONS` specifies number of test + iterations to detect memory leaks. + +### Reporting an issue + +Please attach the output of following information: + +- version of `xmlsec` +- version of `libxmlsec1` +- version of `libxml2` +- output from the command + + ``` bash + pkg-config --cflags xmlsec1 + ``` + +## License + +Unless otherwise noted, all files contained within this project are +licensed under the MIT open source license. See the included `LICENSE` +file or visit [opensource.org](http://opensource.org/licenses/MIT) for +more information. diff --git a/README.rst b/README.rst deleted file mode 100644 index b58c518b..00000000 --- a/README.rst +++ /dev/null @@ -1,218 +0,0 @@ -python-xmlsec -============= - -.. image:: https://travis-ci.org/mehcode/python-xmlsec.png?branch=master - :target: https://travis-ci.org/mehcode/python-xmlsec -.. image:: https://ci.appveyor.com/api/projects/status/ij87xk5wo8a39jua?svg=true - :target: https://ci.appveyor.com/project/hoefling/xmlsec -.. image:: https://github.com/mehcode/python-xmlsec/workflows/manylinux2010/badge.svg - :target: https://github.com/mehcode/python-xmlsec/actions?query=workflow%3A%22manylinux2010%22 -.. image:: https://github.com/mehcode/python-xmlsec/workflows/MacOS/badge.svg - :target: https://github.com/mehcode/python-xmlsec/actions?query=workflow%3A%22MacOS%22 -.. image:: https://codecov.io/gh/mehcode/python-xmlsec/branch/master/graph/badge.svg - :target: https://codecov.io/gh/mehcode/python-xmlsec -.. image:: https://img.shields.io/pypi/v/xmlsec.svg - :target: https://pypi.python.org/pypi/xmlsec -.. image:: https://readthedocs.org/projects/xmlsec/badge/?version=latest - :target: https://xmlsec.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status - -Python bindings for the `XML Security Library `_. - -Documentation -************* - -A documentation for ``xmlsec`` can be found at `xmlsec.readthedocs.io `_. - -Usage -***** - -Check the `examples `_ section in the documentation to see various examples of signing and verifying using the library. - -Requirements -************ -- ``libxml2 >= 2.9.1`` -- ``libxmlsec1 >= 1.2.18`` - -Install -******* - -``xmlsec`` is available on PyPI: - -.. code-block:: bash - - pip install xmlsec - -Depending on your OS, you may need to install the required native -libraries first: - -Linux (Debian) -^^^^^^^^^^^^^^ - -.. code-block:: bash - - apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl - - -Note: There is no required version of LibXML2 for Ubuntu Precise, -so you need to download and install it manually. - -.. code-block:: bash - - wget http://xmlsoft.org/sources/libxml2-2.9.1.tar.gz - tar -xvf libxml2-2.9.1.tar.gz - cd libxml2-2.9.1 - ./configure && make && make install - - -Linux (CentOS) -^^^^^^^^^^^^^^ - -.. code-block:: bash - - yum install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel - - -Linux (Fedora) -^^^^^^^^^^^^^^ - -.. code-block:: bash - - dnf install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel - - -Mac -^^^ - -.. code-block:: bash - - brew install libxml2 libxmlsec1 pkg-config - - -Alpine -^^^^^^ - -.. code-block:: bash - - apk add build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec - - -Troubleshooting -*************** - -Mac -^^^ - -If you get any fatal errors about missing ``.h`` files, update your -``C_INCLUDE_PATH`` environment variable to include the appropriate -files from the ``libxml2`` and ``libxmlsec1`` libraries. - - -Windows -^^^^^^^ - -Starting with 1.3.7, prebuilt wheels are available for Windows, -so running ``pip install xmlsec`` should suffice. If you want -to build from source: - -#. Configure build environment, see `wiki.python.org `_ for more details. - -#. Install from source dist: - - .. code-block:: bash - - pip install xmlsec --no-binary=xmlsec - - -Building from source -******************** - -#. Clone the ``xmlsec`` source code repository to your local computer. - - .. code-block:: bash - - git clone https://github.com/mehcode/python-xmlsec.git - -#. Change into the ``python-xmlsec`` root directory. - - .. code-block:: bash - - cd /path/to/xmlsec - - -#. Install the project and all its dependencies using ``pip``. - - .. code-block:: bash - - pip install . - - -Contributing -************ - -Setting up your environment -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -#. Follow steps 1 and 2 of the `manual installation instructions <#building-from-source>`_. - - -#. Initialize a virtual environment to develop in. - This is done so as to ensure every contributor is working with - close-to-identicial versions of packages. - - .. code-block:: bash - - mkvirtualenv xmlsec - - The ``mkvirtualenv`` command is available from ``virtualenvwrapper`` package which can be installed by following `link `_. - -#. Activate the created virtual environment: - - .. code-block:: bash - - workon xmlsec - -#. Install ``xmlsec`` in development mode with testing enabled. - This will download all dependencies required for running the unit tests. - - .. code-block:: bash - - pip install -r requirements-test.txt - pip install -e "." - - -Running the test suite -^^^^^^^^^^^^^^^^^^^^^^ - -#. `Set up your environment <#setting-up-your-environment>`_. - -#. Run the unit tests. - - .. code-block:: bash - - pytest tests - -#. Tests configuration - - Env variable ``PYXMLSEC_TEST_ITERATIONS`` specifies number of - test iterations to detect memory leaks. - -Reporting an issue -^^^^^^^^^^^^^^^^^^ - -Please attach the output of following information: - -* version of ``xmlsec`` -* version of ``libxmlsec1`` -* version of ``libxml2`` -* output from the command - - .. code-block:: bash - - pkg-config --cflags xmlsec1 - -License -******* - -Unless otherwise noted, all files contained within this project are licensed under the MIT opensource license. -See the included ``LICENSE`` file or visit `opensource.org `_ for more information. diff --git a/typeshed/lxml/__init__.pyi b/build_support/__init__.py similarity index 100% rename from typeshed/lxml/__init__.pyi rename to build_support/__init__.py diff --git a/build_support/build_ext.py b/build_support/build_ext.py new file mode 100644 index 00000000..c4fb5bc9 --- /dev/null +++ b/build_support/build_ext.py @@ -0,0 +1,86 @@ +import os +import sys +from distutils import log +from distutils.errors import DistutilsError + +from setuptools.command.build_ext import build_ext as build_ext_orig + +from .static_build import CrossCompileInfo, StaticBuildHelper + + +class build_ext(build_ext_orig): + def info(self, message): + self.announce(message, level=log.INFO) + + def run(self): + ext = self.ext_map['xmlsec'] + self.debug = os.environ.get('PYXMLSEC_ENABLE_DEBUG', False) + self.static = os.environ.get('PYXMLSEC_STATIC_DEPS', False) + self.size_opt = os.environ.get('PYXMLSEC_OPTIMIZE_SIZE', True) + + if self.static or sys.platform == 'win32': + helper = StaticBuildHelper(self) + helper.prepare(sys.platform) + else: + import pkgconfig + + try: + config = pkgconfig.parse('xmlsec1') + except OSError as error: + raise DistutilsError('Unable to invoke pkg-config.') from error + except pkgconfig.PackageNotFoundError as error: + raise DistutilsError('xmlsec1 is not installed or not in path.') from error + + if config is None or not config.get('libraries'): + raise DistutilsError('Bad or incomplete result returned from pkg-config.') + + ext.define_macros.extend(config['define_macros']) + ext.include_dirs.extend(config['include_dirs']) + ext.library_dirs.extend(config['library_dirs']) + ext.libraries.extend(config['libraries']) + + import lxml + + ext.include_dirs.extend(lxml.get_include()) + + ext.define_macros.extend( + [('MODULE_NAME', self.distribution.metadata.name), ('MODULE_VERSION', self.distribution.metadata.version)] + ) + for key, value in ext.define_macros: + if key == 'XMLSEC_CRYPTO' and not (value.startswith('"') and value.endswith('"')): + ext.define_macros.remove((key, value)) + ext.define_macros.append((key, f'"{value}"')) + break + + if sys.platform == 'win32': + ext.extra_compile_args.append('/Zi') + else: + ext.extra_compile_args.extend( + [ + '-g', + '-std=c99', + '-fPIC', + '-fno-strict-aliasing', + '-Wno-error=declaration-after-statement', + '-Werror=implicit-function-declaration', + ] + ) + + if self.debug: + ext.define_macros.append(('PYXMLSEC_ENABLE_DEBUG', '1')) + if sys.platform == 'win32': + ext.extra_compile_args.append('/Od') + else: + ext.extra_compile_args.append('-Wall') + ext.extra_compile_args.append('-O0') + else: + if self.size_opt: + if sys.platform == 'win32': + ext.extra_compile_args.append('/Os') + else: + ext.extra_compile_args.append('-Os') + + super().run() + + +__all__ = ('CrossCompileInfo', 'build_ext') diff --git a/build_support/network.py b/build_support/network.py new file mode 100644 index 00000000..7ac0bb5e --- /dev/null +++ b/build_support/network.py @@ -0,0 +1,29 @@ +import contextlib +import json +from urllib.request import Request, urlopen + +DEFAULT_USER_AGENT = 'https://github.com/xmlsec/python-xmlsec' +DOWNLOAD_USER_AGENT = 'python-xmlsec build' + + +def make_request(url, github_token=None, json_response=False): + headers = {'User-Agent': DEFAULT_USER_AGENT} + if github_token: + headers['authorization'] = 'Bearer ' + github_token + request = Request(url, headers=headers) + with contextlib.closing(urlopen(request)) as response: + charset = response.headers.get_content_charset() or 'utf-8' + content = response.read().decode(charset) + if json_response: + return json.loads(content) + return content + + +def download_lib(url, filename): + request = Request(url, headers={'User-Agent': DOWNLOAD_USER_AGENT}) + with urlopen(request) as response, open(filename, 'wb') as target: + while True: + chunk = response.read(8192) + if not chunk: + break + target.write(chunk) diff --git a/build_support/releases.py b/build_support/releases.py new file mode 100644 index 00000000..089162e2 --- /dev/null +++ b/build_support/releases.py @@ -0,0 +1,77 @@ +import html.parser +import os +import re +from distutils import log +from distutils.version import StrictVersion as Version + +from .network import make_request + + +class HrefCollector(html.parser.HTMLParser): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.hrefs = [] + + def handle_starttag(self, tag, attrs): + if tag == 'a': + for name, value in attrs: + if name == 'href': + self.hrefs.append(value) + + +def latest_release_from_html(url, matcher): + content = make_request(url) + collector = HrefCollector() + collector.feed(content) + hrefs = collector.hrefs + + def comp(text): + try: + return Version(matcher.match(text).groupdict()['version']) + except (AttributeError, ValueError): + return Version('0.0') + + latest = max(hrefs, key=comp) + return f'{url}/{latest}' + + +def latest_release_from_gnome_org_cache(url, lib_name): + cache_url = f'{url}/cache.json' + cache = make_request(cache_url, json_response=True) + latest_version = cache[2][lib_name][-1] + latest_source = cache[1][lib_name][latest_version]['tar.xz'] + return f'{url}/{latest_source}' + + +def latest_release_json_from_github_api(repo): + api_url = f'https://api.github.com/repos/{repo}/releases/latest' + token = os.environ.get('GH_TOKEN') + if token: + log.info('Using GitHub token to avoid rate limiting') + return make_request(api_url, token, json_response=True) + + +def latest_openssl_release(): + return latest_release_json_from_github_api('openssl/openssl')['tarball_url'] + + +def latest_zlib_release(): + return latest_release_from_html('https://zlib.net/fossils', re.compile('zlib-(?P.*).tar.gz')) + + +def latest_libiconv_release(): + return latest_release_from_html('https://ftpmirror.gnu.org/libiconv', re.compile('libiconv-(?P.*).tar.gz')) + + +def latest_libxml2_release(): + return latest_release_from_gnome_org_cache('https://download.gnome.org/sources/libxml2', 'libxml2') + + +def latest_libxslt_release(): + return latest_release_from_gnome_org_cache('https://download.gnome.org/sources/libxslt', 'libxslt') + + +def latest_xmlsec_release(): + assets = latest_release_json_from_github_api('lsh123/xmlsec')['assets'] + (tar_gz,) = [asset for asset in assets if asset['name'].endswith('.tar.gz')] + return tar_gz['browser_download_url'] diff --git a/build_support/static_build.py b/build_support/static_build.py new file mode 100644 index 00000000..09e2039a --- /dev/null +++ b/build_support/static_build.py @@ -0,0 +1,453 @@ +import multiprocessing +import os +import platform +import subprocess +import sys +import tarfile +import zipfile +from distutils.errors import DistutilsError +from pathlib import Path +from urllib.parse import urljoin +from urllib.request import urlcleanup + +from .network import download_lib +from .releases import ( + latest_libiconv_release, + latest_libxml2_release, + latest_libxslt_release, + latest_openssl_release, + latest_xmlsec_release, + latest_zlib_release, +) + + +class CrossCompileInfo: + def __init__(self, host, arch, compiler): + self.host = host + self.arch = arch + self.compiler = compiler + + @property + def triplet(self): + return f'{self.host}-{self.arch}-{self.compiler}' + + +class StaticBuildHelper: + def __init__(self, builder): + self.builder = builder + self.ext = builder.ext_map['xmlsec'] + self.info = builder.info + self._prepare_directories() + + def prepare(self, platform_name): + self.info(f'starting static build on {sys.platform}') + if platform_name == 'win32': + self._prepare_windows_build() + elif 'linux' in platform_name or 'darwin' in platform_name: + self._prepare_unix_build(platform_name) + else: + raise DistutilsError(f'Unsupported static build platform: {platform_name}') + + def _prepare_directories(self): + buildroot = Path('build', 'tmp') + + prefix_dir = buildroot / 'prefix' + prefix_dir.mkdir(parents=True, exist_ok=True) + self.prefix_dir = prefix_dir.absolute() + + build_libs_dir = buildroot / 'libs' + build_libs_dir.mkdir(exist_ok=True) + self.build_libs_dir = build_libs_dir + + libs_dir = Path(os.environ.get('PYXMLSEC_LIBS_DIR', 'libs')) + libs_dir.mkdir(exist_ok=True) + self.libs_dir = libs_dir + self.info('{:20} {}'.format('Lib sources in:', self.libs_dir.absolute())) + + self.builder.prefix_dir = self.prefix_dir + self.builder.build_libs_dir = self.build_libs_dir + self.builder.libs_dir = self.libs_dir + + def _prepare_windows_build(self): + release_url = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2025.07.10/' + if platform.machine() == 'ARM64': + suffix = 'win-arm64' + elif sys.maxsize > 2**32: + suffix = 'win64' + else: + suffix = 'win32' + + libs = [ + f'libxml2-2.11.9-3.{suffix}.zip', + f'libxslt-1.1.39.{suffix}.zip', + f'zlib-1.3.1.{suffix}.zip', + f'iconv-1.18-1.{suffix}.zip', + f'openssl-3.0.16.pl1.{suffix}.zip', + f'xmlsec-1.3.7.{suffix}.zip', + ] + + for libfile in libs: + url = urljoin(release_url, libfile) + destfile = self.libs_dir / libfile + if destfile.is_file(): + self.info(f'Using local copy of "{url}"') + else: + self.info(f'Retrieving "{url}" to "{destfile}"') + urlcleanup() + download_lib(url, str(destfile)) + + for package in self.libs_dir.glob('*.zip'): + with zipfile.ZipFile(str(package)) as archive: + destdir = self.build_libs_dir + archive.extractall(path=str(destdir)) + + self.ext.define_macros = [ + ('XMLSEC_CRYPTO', '\\"openssl\\"'), + ('__XMLSEC_FUNCTION__', '__FUNCTION__'), + ('XMLSEC_NO_GOST', '1'), + ('XMLSEC_NO_XKMS', '1'), + ('XMLSEC_NO_CRYPTO_DYNAMIC_LOADING', '1'), + ('XMLSEC_CRYPTO_OPENSSL', '1'), + ('UNICODE', '1'), + ('_UNICODE', '1'), + ('LIBXML_ICONV_ENABLED', 1), + ('LIBXML_STATIC', '1'), + ('LIBXSLT_STATIC', 1), + ('XMLSEC_STATIC', 1), + ('inline', '__inline'), + ] + self.ext.libraries = [ + 'libxmlsec_a', + 'libxmlsec-openssl_a', + 'libcrypto', + 'iconv_a', + 'libxslt_a', + 'libexslt_a', + 'libxml2_a', + 'zlib', + 'WS2_32', + 'Advapi32', + 'User32', + 'Gdi32', + 'Crypt32', + ] + self.ext.library_dirs = [str(path.absolute()) for path in self.build_libs_dir.rglob('lib')] + + includes = [path for path in self.build_libs_dir.rglob('include') if path.is_dir()] + includes.append(next(path / 'xmlsec' for path in includes if (path / 'xmlsec').is_dir())) + self.ext.include_dirs = [str(path.absolute()) for path in includes] + + def _prepare_unix_build(self, build_platform): + self._capture_version_overrides() + archives = self._ensure_source_archives() + self._extract_archives(archives) + + env, prefix_arg, ldflags, cross_compile = self._prepare_build_environment(build_platform) + self._build_dependencies(env, prefix_arg, ldflags, cross_compile) + self._configure_extension_for_static(build_platform) + + def _capture_version_overrides(self): + builder = self.builder + builder.openssl_version = os.environ.get('PYXMLSEC_OPENSSL_VERSION', '3.6.0') + builder.libiconv_version = os.environ.get('PYXMLSEC_LIBICONV_VERSION', '1.18') + builder.libxml2_version = os.environ.get('PYXMLSEC_LIBXML2_VERSION', '2.14.6') + builder.libxslt_version = os.environ.get('PYXMLSEC_LIBXSLT_VERSION', '1.1.43') + builder.zlib_version = os.environ.get('PYXMLSEC_ZLIB_VERSION', '1.3.1') + builder.xmlsec1_version = os.environ.get('PYXMLSEC_XMLSEC1_VERSION', '1.3.9') + + def _ensure_source_archives(self): + return [ + self._ensure_source( + name='OpenSSL', + glob='openssl*.tar.gz', + filename='openssl.tar.gz', + version=self.builder.openssl_version, + env_label='PYXMLSEC_OPENSSL_VERSION', + default_url=latest_openssl_release, + version_url=lambda v: f'https://api.github.com/repos/openssl/openssl/tarball/openssl-{v}', + ), + self._ensure_source( + name='zlib', + glob='zlib*.tar.gz', + filename='zlib.tar.gz', + version=self.builder.zlib_version, + env_label='PYXMLSEC_ZLIB_VERSION', + default_url=latest_zlib_release, + version_url=lambda v: f'https://zlib.net/fossils/zlib-{v}.tar.gz', + ), + self._ensure_source( + name='libiconv', + glob='libiconv*.tar.gz', + filename='libiconv.tar.gz', + version=self.builder.libiconv_version, + env_label='PYXMLSEC_LIBICONV_VERSION', + default_url=latest_libiconv_release, + version_url=lambda v: f'https://ftpmirror.gnu.org/libiconv/libiconv-{v}.tar.gz', + ), + self._ensure_source( + name='libxml2', + glob='libxml2*.tar.xz', + filename='libxml2.tar.xz', + version=self.builder.libxml2_version, + env_label='PYXMLSEC_LIBXML2_VERSION', + default_url=latest_libxml2_release, + version_url=lambda v: self._libxml_related_url('libxml2', v), + ), + self._ensure_source( + name='libxslt', + glob='libxslt*.tar.xz', + filename='libxslt.tar.xz', + version=self.builder.libxslt_version, + env_label='PYXMLSEC_LIBXSLT_VERSION', + default_url=latest_libxslt_release, + version_url=lambda v: self._libxml_related_url('libxslt', v), + ), + self._ensure_source( + name='xmlsec1', + glob='xmlsec1*.tar.gz', + filename='xmlsec1.tar.gz', + version=self.builder.xmlsec1_version, + env_label='PYXMLSEC_XMLSEC1_VERSION', + default_url=latest_xmlsec_release, + version_url=lambda v: f'https://github.com/lsh123/xmlsec/releases/download/{v}/xmlsec1-{v}.tar.gz', + ), + ] + + def _ensure_source(self, name, glob, filename, version, env_label, default_url, version_url): + archive = next(self.libs_dir.glob(glob), None) + if archive is not None: + return archive + + self.info('{:10}: {}'.format(name, 'source tar not found, downloading ...')) + archive = self.libs_dir / filename + if version is None: + url = default_url() + self.info('{:10}: {}'.format(name, f'{env_label} unset, downloading latest from {url}')) + else: + url = version_url(version) + self.info('{:10}: {}'.format(name, f'{env_label}={version}, downloading from {url}')) + download_lib(url, str(archive)) + return archive + + def _libxml_related_url(self, lib_name, version): + version_prefix, _ = version.rsplit('.', 1) + return f'https://download.gnome.org/sources/{lib_name}/{version_prefix}/{lib_name}-{version}.tar.xz' + + def _extract_archives(self, archives): + for archive in archives: + self.info(f'Unpacking {archive.name}') + try: + with tarfile.open(str(archive)) as tar: + tar.extractall(path=str(self.build_libs_dir)) + except EOFError as error: + raise DistutilsError(f'Bad {archive.name} downloaded; remove it and try again.') from error + + def _prepare_build_environment(self, build_platform): + prefix_arg = f'--prefix={self.prefix_dir}' + env = os.environ.copy() + + cflags = [] + if env.get('CFLAGS'): + cflags.append(env['CFLAGS']) + cflags.append('-fPIC') + + ldflags = [] + if env.get('LDFLAGS'): + ldflags.append(env['LDFLAGS']) + + cross_compile = None + if build_platform == 'darwin': + arch = self.builder.plat_name.rsplit('-', 1)[1] + if arch != platform.machine() and arch in ('x86_64', 'arm64'): + self.info(f'Cross-compiling for {arch}') + cflags.append(f'-arch {arch}') + ldflags.append(f'-arch {arch}') + cross_compile = CrossCompileInfo('darwin64', arch, 'cc') + major_version, _ = tuple(map(int, platform.mac_ver()[0].split('.')[:2])) + if major_version >= 11 and 'MACOSX_DEPLOYMENT_TARGET' not in env: + env['MACOSX_DEPLOYMENT_TARGET'] = '11.0' + + env['CFLAGS'] = ' '.join(cflags) + env['LDFLAGS'] = ' '.join(ldflags) + return env, prefix_arg, ldflags, cross_compile + + def _build_dependencies(self, env, prefix_arg, ldflags, cross_compile): + self._build_openssl(env, prefix_arg, cross_compile) + self._build_zlib(env, prefix_arg) + + host_arg = [f'--host={cross_compile.arch}'] if cross_compile else [] + self._build_libiconv(env, prefix_arg, host_arg) + self._build_libxml2(env, prefix_arg, host_arg) + self._build_libxslt(env, prefix_arg, host_arg) + + ldflags.append('-lpthread') + env['LDFLAGS'] = ' '.join(ldflags) + self._build_xmlsec1(env, prefix_arg, host_arg) + + def _build_openssl(self, env, prefix_arg, cross_compile): + self.info('Building OpenSSL') + openssl_dir = next(self.build_libs_dir.glob('openssl-*')) + openssl_config_cmd = [prefix_arg, 'no-shared', '-fPIC', '--libdir=lib'] + if platform.machine() == 'riscv64': + # openssl(riscv64): disable ASM to avoid R_RISCV_JAL relocation failure on 3.5.2 + # OpenSSL 3.5.2 enables RISC-V64 AES assembly by default. When we statically + # link libcrypto alongside xmlsec, the AES asm path triggers a link-time error: + # relocation truncated to fit: R_RISCV_JAL against symbol `AES_set_encrypt_key' + # in .../libcrypto.a(libcrypto-lib-aes-riscv64.o) + # This appears to stem from a long-range jump emitted by the AES asm generator + # (see aes-riscv64.pl around L1069), which can exceed the JAL reach when objects + # end up far apart in the final static link. + # As a pragmatic workaround, disable ASM on riscv64 (pass `no-asm`) so the + # portable C implementation is used. This unblocks the build at the cost of + # some crypto performance on riscv64 only. + # Refs: + # - https://github.com/openssl/openssl/blob/0893a62/crypto/aes/asm/aes-riscv64.pl#L1069 + openssl_config_cmd.append('no-asm') + if cross_compile: + openssl_config_cmd.insert(0, './Configure') + openssl_config_cmd.append(cross_compile.triplet) + else: + openssl_config_cmd.insert(0, './config') + subprocess.check_call(openssl_config_cmd, cwd=str(openssl_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(openssl_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install_sw'], cwd=str(openssl_dir), env=env) + + def _build_zlib(self, env, prefix_arg): + self.info('Building zlib') + zlib_dir = next(self.build_libs_dir.glob('zlib-*')) + subprocess.check_call(['./configure', prefix_arg], cwd=str(zlib_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(zlib_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(zlib_dir), env=env) + + def _build_libiconv(self, env, prefix_arg, host_arg): + self.info('Building libiconv') + libiconv_dir = next(self.build_libs_dir.glob('libiconv-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-dependency-tracking', + '--disable-shared', + *host_arg, + ], + cwd=str(libiconv_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libiconv_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libiconv_dir), env=env) + + def _build_libxml2(self, env, prefix_arg, host_arg): + self.info('Building LibXML2') + libxml2_dir = next(self.build_libs_dir.glob('libxml2-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-dependency-tracking', + '--disable-shared', + '--without-lzma', + '--without-python', + f'--with-iconv={self.prefix_dir}', + f'--with-zlib={self.prefix_dir}', + *host_arg, + ], + cwd=str(libxml2_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxml2_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxml2_dir), env=env) + + def _build_libxslt(self, env, prefix_arg, host_arg): + self.info('Building libxslt') + libxslt_dir = next(self.build_libs_dir.glob('libxslt-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-dependency-tracking', + '--disable-shared', + '--without-python', + '--without-crypto', + f'--with-libxml-prefix={self.prefix_dir}', + *host_arg, + ], + cwd=str(libxslt_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxslt_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxslt_dir), env=env) + + def _build_xmlsec1(self, env, prefix_arg, host_arg): + self.info('Building xmlsec1') + xmlsec1_dir = next(self.build_libs_dir.glob('xmlsec1-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-shared', + '--disable-gost', + '--enable-md5', + '--enable-ripemd160', + '--disable-crypto-dl', + '--enable-static=yes', + '--enable-shared=no', + '--enable-static-linking=yes', + '--with-default-crypto=openssl', + f'--with-openssl={self.prefix_dir}', + f'--with-libxml={self.prefix_dir}', + f'--with-libxslt={self.prefix_dir}', + *host_arg, + ], + cwd=str(xmlsec1_dir), + env=env, + ) + include_flags = [ + f'-I{self.prefix_dir / "include"}', + f'-I{self.prefix_dir / "include" / "libxml"}', + ] + subprocess.check_call( + ['make', f'-j{multiprocessing.cpu_count() + 1}', *include_flags], + cwd=str(xmlsec1_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(xmlsec1_dir), env=env) + + def _configure_extension_for_static(self, build_platform): + self.ext.define_macros = [ + ('__XMLSEC_FUNCTION__', '__func__'), + ('XMLSEC_NO_SIZE_T', None), + ('XMLSEC_NO_GOST', '1'), + ('XMLSEC_NO_GOST2012', '1'), + ('XMLSEC_NO_XKMS', '1'), + ('XMLSEC_CRYPTO', '\\"openssl\\"'), + ('XMLSEC_NO_CRYPTO_DYNAMIC_LOADING', '1'), + ('XMLSEC_CRYPTO_OPENSSL', '1'), + ('LIBXML_ICONV_ENABLED', 1), + ('LIBXML_STATIC', 1), + ('LIBXSLT_STATIC', 1), + ('XMLSEC_STATIC', 1), + ('inline', '__inline'), + ('UNICODE', '1'), + ('_UNICODE', '1'), + ] + + self.ext.include_dirs.append(str(self.prefix_dir / 'include')) + self.ext.include_dirs.extend([str(path.absolute()) for path in (self.prefix_dir / 'include').iterdir() if path.is_dir()]) + + self.ext.library_dirs = [] + if build_platform == 'linux': + self.ext.libraries = ['m', 'rt'] + extra_objects = [ + 'libxmlsec1.a', + 'libxslt.a', + 'libxml2.a', + 'libz.a', + 'libxmlsec1-openssl.a', + 'libcrypto.a', + 'libiconv.a', + 'libxmlsec1.a', + ] + self.ext.extra_objects = [str(self.prefix_dir / 'lib' / obj) for obj in extra_objects] + + +__all__ = ('CrossCompileInfo', 'StaticBuildHelper') diff --git a/doc/Makefile b/doc/Makefile index 119469fb..9b6324b7 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -17,4 +17,4 @@ help: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/source/conf.py b/doc/source/conf.py index bb75403b..900b79da 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,20 +1,16 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations -import sys +import importlib.metadata import urllib.request import lxml - -from docutils.nodes import reference -from packaging.version import parse +from docutils.nodes import Text, reference +from packaging.version import Version, parse +from sphinx.addnodes import pending_xref +from sphinx.application import Sphinx +from sphinx.environment import BuildEnvironment from sphinx.errors import ExtensionError -if sys.version_info >= (3, 8): - from importlib import metadata as importlib_metadata -else: - import importlib_metadata - - extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] intersphinx_mapping = {'python': ('https://docs.python.org/3/', None)} @@ -23,40 +19,39 @@ source_suffix = '.rst' master_doc = 'index' -project = u'python-xmlsec' -copyright = u'2020, Oleg Hoefling ' -author = u'Bulat Gaifullin ' -release = importlib_metadata.version('xmlsec') -parsed = parse(release) -version = '{}.{}'.format(parsed.major, parsed.minor) +project = 'python-xmlsec' +copyright = '2020, Oleg Hoefling ' +author = 'Bulat Gaifullin ' +release = importlib.metadata.version('xmlsec') +parsed: Version = parse(release) +version = f'{parsed.major}.{parsed.minor}' -language = None -exclude_patterns = [] +exclude_patterns: list[str] = [] pygments_style = 'sphinx' todo_include_todos = False -html_theme = 'nature' -html_static_path = [] +html_theme = 'furo' +html_static_path: list[str] = [] htmlhelp_basename = 'python-xmlsecdoc' -latex_elements = {} +latex_elements: dict[str, str] = {} latex_documents = [ ( master_doc, 'python-xmlsec.tex', - u'python-xmlsec Documentation', - u'Bulat Gaifullin \\textless{}gaifullinbf@gmail.com\\textgreater{}', + 'python-xmlsec Documentation', + 'Bulat Gaifullin \\textless{}gaifullinbf@gmail.com\\textgreater{}', 'manual', ) ] -man_pages = [(master_doc, 'python-xmlsec', u'python-xmlsec Documentation', [author], 1)] +man_pages = [(master_doc, 'python-xmlsec', 'python-xmlsec Documentation', [author], 1)] texinfo_documents = [ ( master_doc, 'python-xmlsec', - u'python-xmlsec Documentation', + 'python-xmlsec Documentation', author, 'python-xmlsec', 'One line description of project.', @@ -67,15 +62,20 @@ autodoc_member_order = 'groupwise' autodoc_docstring_signature = True + +rst_prolog = """ +.. role:: xml(code) + :language: xml +""" + # LXML crossref'ing stuff: # LXML doesn't have an intersphinx docs, # so we link to lxml.etree._Element explicitly lxml_element_cls_doc_uri = 'https://lxml.de/api/lxml.etree._Element-class.html' -def lxml_element_doc_reference(app, env, node, contnode): - """ - Handle a missing reference only if it is a ``lxml.etree._Element`` ref. +def lxml_element_doc_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: Text) -> reference: + """Handle a missing reference only if it is a ``lxml.etree._Element`` ref. We handle only :class:`lxml.etree._Element` and :class:`~lxml.etree._Element` nodes. """ @@ -84,13 +84,13 @@ def lxml_element_doc_reference(app, env, node, contnode): and node.get('reftarget', None) == 'lxml.etree._Element' and contnode.astext() in ('lxml.etree._Element', '_Element') ): - reftitle = '(in lxml v{})'.format(lxml.__version__) + reftitle = f'(in lxml v{lxml.__version__})' # type: ignore[attr-defined] newnode = reference('', '', internal=False, refuri=lxml_element_cls_doc_uri, reftitle=reftitle) newnode.append(contnode) return newnode -def setup(app): +def setup(app: Sphinx) -> None: # first, check whether the doc URL is still valid if urllib.request.urlopen(lxml_element_cls_doc_uri).getcode() != 200: raise ExtensionError('URL to `lxml.etree._Element` docs is not accesible.') diff --git a/doc/source/examples/decrypt.py b/doc/source/examples/decrypt.py index e107756f..cb474d22 100644 --- a/doc/source/examples/decrypt.py +++ b/doc/source/examples/decrypt.py @@ -6,7 +6,7 @@ key = xmlsec.Key.from_file('rsakey.pem', xmlsec.constants.KeyDataFormatPem) manager.add_key(key) enc_ctx = xmlsec.EncryptionContext(manager) -root = etree.parse("enc1-res.xml").getroot() -enc_data = xmlsec.tree.find_child(root, "EncryptedData", xmlsec.constants.EncNs) +root = etree.parse('enc1-res.xml').getroot() +enc_data = xmlsec.tree.find_child(root, 'EncryptedData', xmlsec.constants.EncNs) decrypted = enc_ctx.decrypt(enc_data) print(etree.tostring(decrypted)) diff --git a/doc/source/examples/encrypt.py b/doc/source/examples/encrypt.py index f69d4613..2a92264e 100644 --- a/doc/source/examples/encrypt.py +++ b/doc/source/examples/encrypt.py @@ -2,39 +2,32 @@ import xmlsec -manager = xmlsec.KeysManager() -key = xmlsec.Key.from_file('rsacert.pem', xmlsec.constants.KeyDataFormatCertPem, None) -manager.add_key(key) -template = etree.parse('enc1-doc.xml').getroot() +with open('enc1-doc.xml') as fp: + template = etree.parse(fp).getroot() + enc_data = xmlsec.template.encrypted_data_create( template, xmlsec.constants.TransformAes128Cbc, type=xmlsec.constants.TypeEncElement, - ns="xenc", + ns='xenc', ) xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) -key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns="dsig") +key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns='dsig') enc_key = xmlsec.template.add_encrypted_key(key_info, xmlsec.constants.TransformRsaOaep) xmlsec.template.encrypted_data_ensure_cipher_value(enc_key) data = template.find('./Data') # Encryption +manager = xmlsec.KeysManager() +key = xmlsec.Key.from_file('rsacert.pem', xmlsec.constants.KeyDataFormatCertPem, None) +manager.add_key(key) + enc_ctx = xmlsec.EncryptionContext(manager) -enc_ctx.key = xmlsec.Key.generate( - xmlsec.constants.KeyDataAes, 128, xmlsec.constants.KeyDataTypeSession -) +enc_ctx.key = xmlsec.Key.generate(xmlsec.constants.KeyDataAes, 128, xmlsec.constants.KeyDataTypeSession) enc_data = enc_ctx.encrypt_xml(enc_data, data) -enc_method = xmlsec.tree.find_child( - enc_data, xmlsec.constants.NodeEncryptionMethod, xmlsec.constants.EncNs -) -key_info = xmlsec.tree.find_child( - enc_data, xmlsec.constants.NodeKeyInfo, xmlsec.constants.DSigNs -) -enc_method = xmlsec.tree.find_node( - key_info, xmlsec.constants.NodeEncryptionMethod, xmlsec.constants.EncNs -) -cipher_value = xmlsec.tree.find_node( - key_info, xmlsec.constants.NodeCipherValue, xmlsec.constants.EncNs -) +enc_method = xmlsec.tree.find_child(enc_data, xmlsec.constants.NodeEncryptionMethod, xmlsec.constants.EncNs) +key_info = xmlsec.tree.find_child(enc_data, xmlsec.constants.NodeKeyInfo, xmlsec.constants.DSigNs) +enc_method = xmlsec.tree.find_node(key_info, xmlsec.constants.NodeEncryptionMethod, xmlsec.constants.EncNs) +cipher_value = xmlsec.tree.find_node(key_info, xmlsec.constants.NodeCipherValue, xmlsec.constants.EncNs) print(etree.tostring(cipher_value)) diff --git a/doc/source/examples/sign.py b/doc/source/examples/sign.py index 4529bc8a..519c13a0 100644 --- a/doc/source/examples/sign.py +++ b/doc/source/examples/sign.py @@ -2,7 +2,8 @@ import xmlsec -template = etree.parse('sign1-tmpl.xml').getroot() +with open('sign1-tmpl.xml') as fp: + template = etree.parse(fp).getroot() signature_node = xmlsec.tree.find_node(template, xmlsec.constants.NodeSignature) ctx = xmlsec.SignatureContext() diff --git a/doc/source/examples/sign_binary.py b/doc/source/examples/sign_binary.py index 4e6c0e00..275c6e40 100644 --- a/doc/source/examples/sign_binary.py +++ b/doc/source/examples/sign_binary.py @@ -1,5 +1,3 @@ -from lxml import etree - import xmlsec ctx = xmlsec.SignatureContext() diff --git a/doc/source/examples/verify.py b/doc/source/examples/verify.py index 8629c550..c3240c99 100644 --- a/doc/source/examples/verify.py +++ b/doc/source/examples/verify.py @@ -2,8 +2,10 @@ import xmlsec -template = etree.parse('sign1-res.xml').getroot() -xmlsec.tree.add_ids(template, ["ID"]) +with open('sign1-res.xml') as fp: + template = etree.parse(fp).getroot() + +xmlsec.tree.add_ids(template, ['ID']) signature_node = xmlsec.tree.find_node(template, xmlsec.constants.NodeSignature) # Create a digital signature context (no key manager is needed). ctx = xmlsec.SignatureContext() diff --git a/doc/source/examples/verify_binary.py b/doc/source/examples/verify_binary.py index 06c2b727..1f888b99 100644 --- a/doc/source/examples/verify_binary.py +++ b/doc/source/examples/verify_binary.py @@ -1,5 +1,3 @@ -from lxml import etree - import xmlsec ctx = xmlsec.SignatureContext() diff --git a/doc/source/index.rst b/doc/source/index.rst index 5cc758b9..e08f47d9 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,9 +3,6 @@ You can adapt this file completely to your liking, but it should at least contain the root ``toctree`` directive. -.. role:: xml(code) - :language: xml - Welcome to python-xmlsec's documentation! ========================================= diff --git a/doc/source/install.rst b/doc/source/install.rst index 834b9acb..c892a3ea 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -59,7 +59,7 @@ Alpine .. code-block:: bash - apk add build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec + apk add build-base openssl libffi-dev openssl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec Troubleshooting diff --git a/doc/source/modules/constants.rst b/doc/source/modules/constants.rst index 4a63fcd7..3df6b50f 100644 --- a/doc/source/modules/constants.rst +++ b/doc/source/modules/constants.rst @@ -49,7 +49,11 @@ KeyData .. data:: xmlsec.constants.KeyDataEcdsa - The ECDSA key klass. + (Deprecated. The EC key klass) The ECDSA key klass. + +.. data:: xmlsec.constants.KeyDataEc + + The EC key klass. .. data:: xmlsec.constants.KeyDataHmac @@ -166,12 +170,6 @@ Namespaces .. data:: xmlsec.constants.XPointerNs :annotation: = 'http://www.w3.org/2001/04/xmldsig-more/xptr' -.. data:: xmlsec.constants.Soap11Ns - :annotation: = 'http://schemas.xmlsoap.org/soap/envelope/' - -.. data:: xmlsec.constants.Soap12Ns - :annotation: = 'http://www.w3.org/2002/06/soap-envelope' - .. data:: xmlsec.constants.NsExcC14N :annotation: = 'http://www.w3.org/2001/10/xml-exc-c14n#' diff --git a/doc/source/requirements.txt b/doc/source/requirements.txt index 09ff0002..ffb2b6d3 100644 --- a/doc/source/requirements.txt +++ b/doc/source/requirements.txt @@ -1,4 +1,5 @@ -lxml>=3.8 +lxml==6.0.2 importlib_metadata;python_version < '3.8' packaging Sphinx>=3 +furo>=2021.4.11b34 diff --git a/doc/source/sphinx-pr-6916.diff b/doc/source/sphinx-pr-6916.diff deleted file mode 100644 index e7040a0f..00000000 --- a/doc/source/sphinx-pr-6916.diff +++ /dev/null @@ -1,46 +0,0 @@ -diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py -index bc9bf49a74..4804c89c52 100644 ---- a/sphinx/environment/__init__.py -+++ b/sphinx/environment/__init__.py -@@ -46,6 +46,7 @@ - default_settings = { - 'embed_stylesheet': False, - 'cloak_email_addresses': True, -+ 'syntax_highlight': 'short', - 'pep_base_url': 'https://www.python.org/dev/peps/', - 'pep_references': None, - 'rfc_base_url': 'https://tools.ietf.org/html/', -diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py -index 85eeb43..80f1eea 100644 ---- a/sphinx/writers/html.py -+++ b/sphinx/writers/html.py -@@ -494,8 +494,11 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): - self.body.append(self.starttag(node, 'kbd', '', - CLASS='docutils literal notranslate')) - else: -+ classes = 'docutils literal notranslate' -+ if 'code' in node['classes']: -+ classes += ' highlight' - self.body.append(self.starttag(node, 'code', '', -- CLASS='docutils literal notranslate')) -+ CLASS=classes)) - self.protect_literal_text += 1 - - def depart_literal(self, node: Element) -> None: -diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py -index 80cedd3..470f559 100644 ---- a/sphinx/writers/html5.py -+++ b/sphinx/writers/html5.py -@@ -446,8 +446,11 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): - self.body.append(self.starttag(node, 'kbd', '', - CLASS='docutils literal notranslate')) - else: -+ classes = 'docutils literal notranslate' -+ if 'code' in node['classes']: -+ classes += ' highlight' - self.body.append(self.starttag(node, 'code', '', -- CLASS='docutils literal notranslate')) -+ CLASS=classes)) - self.protect_literal_text += 1 - - def depart_literal(self, node: Element) -> None: diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index ce190e35..00000000 --- a/mypy.ini +++ /dev/null @@ -1,17 +0,0 @@ -[mypy] -files = src -mypy_path = typeshed/ -ignore_missing_imports = False -warn_unused_configs = True -disallow_subclassing_any = True -disallow_any_generics = True -disallow_untyped_calls = True -disallow_untyped_defs = True -disallow_incomplete_defs = True -check_untyped_defs = True -disallow_untyped_decorators = True -no_implicit_optional = True -warn_redundant_casts = True -warn_unused_ignores = True -warn_return_any = True -no_implicit_reexport = True diff --git a/pyproject.toml b/pyproject.toml index 636c52c9..7c7b4bf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,33 +1,131 @@ -[tool.black] -line_length = 130 -skip-string-normalization = true -target_version = ['py38'] -include = '\.pyi?$' -exclude = ''' - -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.mypy_cache - | \.tox - | build - | dist - )/ -) -''' - -[tool.isort] -force_alphabetical_sort_within_sections = true -recursive = true -line_length = 130 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true -combine_as_imports = true -known_first_party = ['xmlsec'] -known_third_party = ['lxml', 'pytest', '_pytest', 'hypothesis'] - [build-system] -requires = ['setuptools>=42', 'wheel', 'setuptools_scm[toml]>=3.4'] +requires = ["setuptools==80.9.0", "wheel", "setuptools_scm[toml]>=3.4", "pkgconfig>=1.5.1", "lxml==6.0.2"] + +[tool.mypy] +files = ['src'] +ignore_missing_imports = false +warn_unused_configs = true +disallow_subclassing_any = true +disallow_any_generics = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +disallow_any_unimported = true +strict_optional = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +warn_no_return = true +no_implicit_reexport = true +show_error_codes = true + +[tool.ruff] +# Maximum line length, same as your original Black + Flake8 config +line-length = 130 + +# Target Python version (used for autofixes and style rules) +target-version = "py39" + +# Directories and files to exclude from linting and formatting +exclude = [ + ".venv*", # virtual environments + ".git", # git directory + "build", # build output + "dist", # distribution packages + "libs", # vendor libraries + ".eggs", # setuptools egg folders + ".direnv*", # direnv environments + "*_pb2.pyi" # protobuf-generated type stubs +] + +[tool.ruff.lint] +# Enable rule categories: +# E = pycodestyle (style issues, like indentation, whitespace, etc.) +# F = pyflakes (unused imports, undefined names) +# I = isort (import sorting) +# B = flake8-bugbear (common bugs & anti-patterns) +# UP = pyupgrade (auto-upgrade syntax for newer Python) +# SIM = flake8-simplify (simplifiable code patterns) +# RUF = Ruff-native rules (extra, performance-optimized checks) +select = ["E", "F", "I", "B", "UP", "SIM", "RUF"] +# TODO: Add more rule categories as needed, e.g.: +# D = pydocstyle (docstring format/style issues) + +[tool.ruff.lint.per-file-ignores] +"*.pyi" = [ + # Ignore formatting and import errors in stub files + "E301", # expected 1 blank line, found 0 + "E302", # expected 2 blank lines, found 1 + "E305", # expected 2 blank lines after class or function + "E501", # line too long + "E701", # multiple statements on one line + "F401", # unused import + "F811", # redefinition of unused name + "F822" # undefined name in `__all__` +] +"doc/source/conf.py" = [ + "D1" # missing docstring in public module/class/function +] +"doc/source/examples/*.py" = [ + "D1", # allow missing docstrings in examples + "E501" # allow long lines in code examples +] +"tests/*.py" = [ + "D1" # allow missing docstrings in test files +] + +[tool.ruff.format] +# Always use single quotes (e.g., 'text' instead of "text") +quote-style = "single" + +# Format code with or without trailing commas +# true = prefer trailing commas where valid +skip-magic-trailing-comma = false + +# Enforce Unix-style line endings (LF) +line-ending = "lf" + +[tool.cibuildwheel] +build = [ + "cp39-*", + "cp310-*", + "cp311-*", + "cp312-*", + "cp313-*", + "cp314-*" +] +build-verbosity = 1 +build-frontend = "build" +skip = [ + "pp*", # Skips PyPy builds (pp38-*, pp39-*, etc.) + "*musllinux_riscv64" # maturin and ruff currently don’t support the musl + riscv64 target +] +test-command = "pytest -v --color=yes {package}/tests" +before-test = "pip install -r requirements-test.txt" +test-skip = "*-macosx_arm64" + +[tool.cibuildwheel.environment] +PYXMLSEC_STATIC_DEPS = "true" + +[tool.cibuildwheel.linux] +archs = ["x86_64", "aarch64", "riscv64"] +environment-pass = [ + "PYXMLSEC_LIBXML2_VERSION", + "PYXMLSEC_LIBXSLT_VERSION", + "PYXMLSEC_STATIC_DEPS", + "GH_TOKEN" +] + +[tool.cibuildwheel.macos] +archs = ["x86_64", "arm64"] +before-all = "brew install perl" + +[tool.cibuildwheel.windows] +archs = ["AMD64"] + +[[tool.cibuildwheel.overrides]] +select = "*-manylinux*" +before-all = "yum install -y perl-core" diff --git a/requirements-test.txt b/requirements-test.txt index dde7de3d..ad135d97 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,5 @@ -r requirements.txt -pytest>=4.6.9 -hypothesis + +pytest==8.4.1 +lxml-stubs==0.5.1 +ruff[format]==0.13.0 diff --git a/requirements.txt b/requirements.txt index 827d75e0..8221c374 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -pkgconfig -lxml >= 3.8.0 +lxml==6.0.2 diff --git a/setup.cfg b/setup.cfg index ada30e9b..8762c654 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,9 @@ [metadata] -description-file = README.rst +description_file = README.md [bdist_rpm] release = 1 -build-requires = pkg-config xmlsec1-devel libxml2-devel xmlsec1-openssl-devel +build_requires = pkg-config xmlsec1-devel libxml2-devel xmlsec1-openssl-devel group = Development/Libraries requires = xmlsec1 xmlsec1-openssl @@ -12,8 +12,11 @@ source-dir = doc/source build-dir = doc/build all_files = 1 -[upload_docs] -upload-dir = doc/build/html - -[flake8] -max-line-length = 130 +# [flake8] +# per-file-ignores = +# *.pyi: E301, E302, E305, E501, E701, F401, F822 +# doc/source/conf.py: D1 +# doc/source/examples/*.py: D1, E501 +# tests/*.py: D1 +# exclude = .venv*,.git,*_pb2.pyi,build,dist,libs,.eggs,.direnv* +# max-line-length = 130 diff --git a/setup.py b/setup.py index 6906174d..4100a52b 100644 --- a/setup.py +++ b/setup.py @@ -1,430 +1,17 @@ -import io -import multiprocessing -import os -import subprocess -import sys -import tarfile -import zipfile -from distutils import log -from distutils.errors import DistutilsError +from pathlib import Path from setuptools import Extension, setup -from setuptools.command.build_ext import build_ext as build_ext_orig -if sys.version_info >= (3, 4): - from urllib.request import urlcleanup, urljoin, urlretrieve -else: - from urllib import urlcleanup, urlretrieve - from urlparse import urljoin - - -class build_ext(build_ext_orig, object): - def info(self, message): - self.announce(message, level=log.INFO) - - def run(self): - if sys.version_info >= (3, 4): - from pathlib import Path - else: - from pathlib2 import Path - - ext = self.ext_map['xmlsec'] - self.debug = os.environ.get('DEBUG', False) - self.static = os.environ.get('STATIC_DEPS', False) - - if self.static or sys.platform == 'win32': - self.info('starting static build on {}'.format(sys.platform)) - buildroot = Path('build', 'tmp') - - self.prefix_dir = buildroot / 'prefix' - self.prefix_dir.mkdir(parents=True, exist_ok=True) - self.prefix_dir = self.prefix_dir.absolute() - - self.build_libs_dir = buildroot / 'libs' - self.build_libs_dir.mkdir(exist_ok=True) - - self.libs_dir = Path(os.environ.get('LIBS_DIR', 'libs')) - self.libs_dir.mkdir(exist_ok=True) - - if sys.platform == 'win32': - self.prepare_static_build_win() - elif 'linux' in sys.platform: - self.prepare_static_build_linux() - else: - import pkgconfig - - try: - config = pkgconfig.parse('xmlsec1') - except EnvironmentError: - raise DistutilsError('Unable to invoke pkg-config.') - except pkgconfig.PackageNotFoundError: - raise DistutilsError('xmlsec1 is not installed or not in path.') - - if config is None or not config.get('libraries'): - raise DistutilsError('Bad or incomplete result returned from pkg-config.') - - ext.define_macros.extend(config['define_macros']) - ext.include_dirs.extend(config['include_dirs']) - ext.library_dirs.extend(config['library_dirs']) - ext.libraries.extend(config['libraries']) - - import lxml - - ext.include_dirs.extend(lxml.get_include()) - - ext.define_macros.extend( - [('MODULE_NAME', self.distribution.metadata.name), ('MODULE_VERSION', self.distribution.metadata.version)] - ) - # escape the XMLSEC_CRYPTO macro value, see mehcode/python-xmlsec#141 - for (key, value) in ext.define_macros: - if key == 'XMLSEC_CRYPTO' and not (value.startswith('"') and value.endswith('"')): - ext.define_macros.remove((key, value)) - ext.define_macros.append((key, '"{0}"'.format(value))) - break - - if sys.platform == 'win32': - ext.extra_compile_args.append('/Zi') - else: - ext.extra_compile_args.extend( - [ - '-g', - '-std=c99', - '-fPIC', - '-fno-strict-aliasing', - '-Wno-error=declaration-after-statement', - '-Werror=implicit-function-declaration', - ] - ) - - if self.debug: - ext.extra_compile_args.append('-Wall') - ext.extra_compile_args.append('-O0') - ext.define_macros.append(('PYXMLSEC_ENABLE_DEBUG', '1')) - else: - ext.extra_compile_args.append('-Os') - - super(build_ext, self).run() - - def prepare_static_build_win(self): - release_url = 'https://github.com/bgaifullin/libxml2-win-binaries/releases/download/v2018.08/' - if sys.version_info < (3, 5): - if sys.maxsize > 2147483647: - suffix = 'vs2008.win64' - else: - suffix = "vs2008.win32" - else: - if sys.maxsize > 2147483647: - suffix = "win64" - else: - suffix = "win32" - - libs = [ - 'libxml2-2.9.4.{}.zip'.format(suffix), - 'libxslt-1.1.29.{}.zip'.format(suffix), - 'zlib-1.2.8.{}.zip'.format(suffix), - 'iconv-1.14.{}.zip'.format(suffix), - 'openssl-1.0.1.{}.zip'.format(suffix), - 'xmlsec-1.2.24.{}.zip'.format(suffix), - ] - - for libfile in libs: - url = urljoin(release_url, libfile) - destfile = self.libs_dir / libfile - if destfile.is_file(): - self.info('Using local copy of "{}"'.format(url)) - else: - self.info('Retrieving "{}" to "{}"'.format(url, destfile)) - urlcleanup() # work around FTP bug 27973 in Py2.7.12+ - urlretrieve(url, str(destfile)) - - for p in self.libs_dir.glob('*.zip'): - with zipfile.ZipFile(str(p)) as f: - destdir = self.build_libs_dir - f.extractall(path=str(destdir)) - - ext = self.ext_map['xmlsec'] - ext.define_macros = [ - ('XMLSEC_CRYPTO', '\\"openssl\\"'), - ('__XMLSEC_FUNCTION__', '__FUNCTION__'), - ('XMLSEC_NO_GOST', '1'), - ('XMLSEC_NO_XKMS', '1'), - ('XMLSEC_NO_CRYPTO_DYNAMIC_LOADING', '1'), - ('XMLSEC_CRYPTO_OPENSSL', '1'), - ('UNICODE', '1'), - ('_UNICODE', '1'), - ('LIBXML_ICONV_ENABLED', 1), - ('LIBXML_STATIC', '1'), - ('LIBXSLT_STATIC', '1'), - ('XMLSEC_STATIC', '1'), - ('inline', '__inline'), - ] - ext.libraries = [ - 'libxmlsec_a', - 'libxmlsec-openssl_a', - 'libeay32', - 'iconv_a', - 'libxslt_a', - 'libexslt_a', - 'libxml2_a', - 'zlib', - 'WS2_32', - 'Advapi32', - 'User32', - 'Gdi32', - 'Crypt32', - ] - ext.library_dirs = [str(p.absolute()) for p in self.build_libs_dir.rglob('lib')] - - includes = [p for p in self.build_libs_dir.rglob('include') if p.is_dir()] - includes.append(next(p / 'xmlsec' for p in includes if (p / 'xmlsec').is_dir())) - ext.include_dirs = [str(p.absolute()) for p in includes] - - def prepare_static_build_linux(self): - self.openssl_version = os.environ.get('OPENSSL_VERSION', '1.1.1g') - self.libiconv_version = os.environ.get('LIBICONV_VERSION', '1.16') - self.libxml2_version = os.environ.get('LIBXML2_VERSION', None) - self.libxslt_version = os.environ.get('LIBXLST_VERSION', None) - self.zlib_version = os.environ.get('ZLIB_VERSION', '1.2.11') - self.xmlsec1_version = os.environ.get('XMLSEC1_VERSION', '1.2.30') - - self.info('Settings:') - self.info('{:20} {}'.format('Lib sources in:', self.libs_dir.absolute())) - self.info('{:20} {}'.format('zlib version:', self.zlib_version)) - self.info('{:20} {}'.format('libiconv version:', self.libiconv_version)) - self.info('{:20} {}'.format('libxml2 version:', self.libxml2_version or 'unset, using latest')) - self.info('{:20} {}'.format('libxslt version:', self.libxslt_version or 'unset, using latest')) - self.info('{:20} {}'.format('xmlsec1 version:', self.xmlsec1_version)) - - # fetch openssl - openssl_tar = next(self.libs_dir.glob('openssl*.tar.gz'), None) - if openssl_tar is None: - self.info('OpenSSL source tar not found, downloading ...') - openssl_tar = self.libs_dir / 'openssl.tar.gz' - urlretrieve('https://www.openssl.org/source/openssl-{}.tar.gz'.format(self.openssl_version), str(openssl_tar)) - - # fetch zlib - zlib_tar = next(self.libs_dir.glob('zlib*.tar.gz'), None) - if zlib_tar is None: - self.info('zlib source tar not found, downloading ...') - zlib_tar = self.libs_dir / 'zlib.tar.gz' - urlretrieve('https://zlib.net/zlib-{}.tar.gz'.format(self.zlib_version), str(zlib_tar)) - - # fetch libiconv - libiconv_tar = next(self.libs_dir.glob('libiconv*.tar.gz'), None) - if libiconv_tar is None: - self.info('libiconv source tar not found, downloading ...') - libiconv_tar = self.libs_dir / 'libiconv.tar.gz' - urlretrieve( - 'https://ftp.gnu.org/pub/gnu/libiconv/libiconv-{}.tar.gz'.format(self.libiconv_version), str(libiconv_tar) - ) - - # fetch libxml2 - libxml2_tar = next(self.libs_dir.glob('libxml2*.tar.gz'), None) - if libxml2_tar is None: - self.info('Libxml2 source tar not found, downloading ...') - if self.libxml2_version is None: - url = 'http://xmlsoft.org/sources/LATEST_LIBXML2' - else: - url = 'http://xmlsoft.org/sources/libxml2-{}.tar.gz'.format(self.libxml2_version) - libxml2_tar = self.libs_dir / 'libxml2.tar.gz' - urlretrieve(url, str(libxml2_tar)) - - # fetch libxslt - libxslt_tar = next(self.libs_dir.glob('libxslt*.tar.gz'), None) - if libxslt_tar is None: - self.info('libxslt source tar not found, downloading ...') - if self.libxslt_version is None: - url = 'http://xmlsoft.org/sources/LATEST_LIBXSLT' - else: - url = 'http://xmlsoft.org/sources/libxslt-{}.tar.gz'.format(self.libxslt_version) - libxslt_tar = self.libs_dir / 'libxslt.tar.gz' - urlretrieve(url, str(libxslt_tar)) - - # fetch xmlsec1 - xmlsec1_tar = next(self.libs_dir.glob('xmlsec1*.tar.gz'), None) - if xmlsec1_tar is None: - self.info('xmlsec1 source tar not found, downloading ...') - url = 'http://www.aleksey.com/xmlsec/download/xmlsec1-{}.tar.gz'.format(self.xmlsec1_version) - xmlsec1_tar = self.libs_dir / 'xmlsec1.tar.gz' - urlretrieve(url, str(xmlsec1_tar)) - - for file in (openssl_tar, zlib_tar, libiconv_tar, libxml2_tar, libxslt_tar, xmlsec1_tar): - self.info('Unpacking {}'.format(file.name)) - try: - with tarfile.open(str(file)) as tar: - tar.extractall(path=str(self.build_libs_dir)) - except EOFError: - raise DistutilsError('Bad {} downloaded; remove it and try again.'.format(file.name)) - - prefix_arg = '--prefix={}'.format(self.prefix_dir) - - cflags = ['-fPIC'] - env = os.environ.copy() - if 'CFLAGS' in env: - env['CFLAGS'].append(' '.join(cflags)) - else: - env['CFLAGS'] = ' '.join(cflags) - - self.info('Building OpenSSL') - openssl_dir = next(self.build_libs_dir.glob('openssl-*')) - subprocess.check_output(['./config', prefix_arg, 'no-shared', '-fPIC'], cwd=str(openssl_dir), env=env) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(openssl_dir), env=env) - subprocess.check_output( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install_sw'], cwd=str(openssl_dir), env=env - ) - - self.info('Building zlib') - zlib_dir = next(self.build_libs_dir.glob('zlib-*')) - subprocess.check_output(['./configure', prefix_arg], cwd=str(zlib_dir), env=env) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(zlib_dir), env=env) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(zlib_dir), env=env) - - self.info('Building libiconv') - libiconv_dir = next(self.build_libs_dir.glob('libiconv-*')) - subprocess.check_output( - ['./configure', prefix_arg, '--disable-dependency-tracking', '--disable-shared'], cwd=str(libiconv_dir), env=env - ) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libiconv_dir), env=env) - subprocess.check_output( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libiconv_dir), env=env - ) - - self.info('Building LibXML2') - libxml2_dir = next(self.build_libs_dir.glob('libxml2-*')) - subprocess.check_output( - [ - './configure', - prefix_arg, - '--disable-dependency-tracking', - '--disable-shared', - '--enable-rebuild-docs=no', - '--without-lzma', - '--without-python', - '--with-iconv={}'.format(self.prefix_dir), - '--with-zlib={}'.format(self.prefix_dir), - ], - cwd=str(libxml2_dir), - env=env, - ) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libxml2_dir), env=env) - subprocess.check_output( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libxml2_dir), env=env - ) - - self.info('Building libxslt') - libxslt_dir = next(self.build_libs_dir.glob('libxslt-*')) - subprocess.check_output( - [ - './configure', - prefix_arg, - '--disable-dependency-tracking', - '--disable-shared', - '--without-python', - '--without-crypto', - '--with-libxml-prefix={}'.format(self.prefix_dir), - ], - cwd=str(libxslt_dir), - env=env, - ) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libxslt_dir), env=env) - subprocess.check_output( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libxslt_dir), env=env - ) - - self.info('Building xmlsec1') - if 'LDFLAGS' in env: - env['LDFLAGS'].append(' -lpthread') - else: - env['LDFLAGS'] = '-lpthread' - xmlsec1_dir = next(self.build_libs_dir.glob('xmlsec1-*')) - subprocess.check_output( - [ - './configure', - prefix_arg, - '--disable-shared', - '--disable-gost', - '--disable-crypto-dl', - '--enable-static=yes', - '--enable-shared=no', - '--enable-static-linking=yes', - '--with-default-crypto=openssl', - '--with-openssl={}'.format(self.prefix_dir), - '--with-libxml={}'.format(self.prefix_dir), - '--with-libxslt={}'.format(self.prefix_dir), - ], - cwd=str(xmlsec1_dir), - env=env, - ) - subprocess.check_output( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1)] - + ['-I{}'.format(str(self.prefix_dir / 'include')), '-I{}'.format(str(self.prefix_dir / 'include' / 'libxml'))], - cwd=str(xmlsec1_dir), - env=env, - ) - subprocess.check_output( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(xmlsec1_dir), env=env - ) - - ext = self.ext_map['xmlsec'] - ext.define_macros = [ - ('__XMLSEC_FUNCTION__', '__func__'), - ('XMLSEC_NO_SIZE_T', None), - ('XMLSEC_NO_GOST', '1'), - ('XMLSEC_NO_GOST2012', '1'), - ('XMLSEC_NO_XKMS', '1'), - ('XMLSEC_CRYPTO', '\\"openssl\\"'), - ('XMLSEC_NO_CRYPTO_DYNAMIC_LOADING', '1'), - ('XMLSEC_CRYPTO_OPENSSL', '1'), - ('LIBXML_ICONV_ENABLED', 1), - ('LIBXML_STATIC', '1'), - ('LIBXSLT_STATIC', '1'), - ('XMLSEC_STATIC', '1'), - ('inline', '__inline'), - ('UNICODE', '1'), - ('_UNICODE', '1'), - ] - - ext.include_dirs.append(str(self.prefix_dir / 'include')) - ext.include_dirs.extend([str(p.absolute()) for p in (self.prefix_dir / 'include').iterdir() if p.is_dir()]) - - ext.library_dirs = [] - ext.libraries = ['m', 'rt'] - extra_objects = [ - 'libxmlsec1.a', - 'libxslt.a', - 'libxml2.a', - 'libz.a', - 'libxmlsec1-openssl.a', - 'libcrypto.a', - 'libiconv.a', - 'libxmlsec1.a', - ] - ext.extra_objects = [str(self.prefix_dir / 'lib' / o) for o in extra_objects] - - -if sys.version_info >= (3, 4): - from pathlib import Path - - src_root = Path(__file__).parent / 'src' - sources = [str(p.absolute()) for p in src_root.rglob('*.c')] -else: - import fnmatch - - src_root = os.path.join(os.path.dirname(__file__), 'src') - sources = [] - for root, _, files in os.walk(src_root): - for file in fnmatch.filter(files, '*.c'): - sources.append(os.path.join(root, file)) +from build_support.build_ext import build_ext +src_root = Path(__file__).parent / 'src' +sources = [str(path.absolute()) for path in src_root.rglob('*.c')] pyxmlsec = Extension('xmlsec', sources=sources) -setup_reqs = ['setuptools_scm[toml]>=3.4', 'pkgconfig', 'lxml>=3.8'] - -if sys.version_info < (3, 4): - setup_reqs.append('pathlib2') +setup_reqs = ['setuptools_scm[toml]>=3.4', 'pkgconfig>=1.5.1', 'lxml>=3.8'] -with io.open('README.rst', encoding='utf-8') as f: - long_desc = f.read() +with open('README.md', encoding='utf-8') as readme: + long_desc = readme.read() setup( @@ -432,16 +19,22 @@ def prepare_static_build_linux(self): use_scm_version=True, description='Python bindings for the XML Security Library', long_description=long_desc, + long_description_content_type='text/markdown', ext_modules=[pyxmlsec], cmdclass={'build_ext': build_ext}, - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', + python_requires='>=3.9', setup_requires=setup_reqs, install_requires=['lxml>=3.8'], - author="Bulat Gaifullin", + author='Bulat Gaifullin', author_email='support@mehcode.com', maintainer='Oleg Hoefling', maintainer_email='oleg.hoefling@gmail.com', url='https://github.com/mehcode/python-xmlsec', + project_urls={ + 'Documentation': 'https://xmlsec.readthedocs.io', + 'Source': 'https://github.com/mehcode/python-xmlsec', + 'Changelog': 'https://github.com/mehcode/python-xmlsec/releases', + }, license='MIT', keywords=['xmlsec'], classifiers=[ @@ -451,12 +44,13 @@ def prepare_static_build_linux(self): 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: C', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Topic :: Text Processing :: Markup :: XML', 'Typing :: Typed', ], diff --git a/src/common.h b/src/common.h index 243ed651..a6176551 100644 --- a/src/common.h +++ b/src/common.h @@ -13,7 +13,7 @@ #include "debug.h" #ifndef MODULE_NAME -#define MODULE_NAME "xmlsec" +#define MODULE_NAME xmlsec #endif #define JOIN(X,Y) DO_JOIN1(X,Y) diff --git a/src/constants.c b/src/constants.c index 02f5250c..bd1fa5e0 100644 --- a/src/constants.c +++ b/src/constants.c @@ -27,7 +27,7 @@ static PyObject* PyXmlSec_Transform__str__(PyObject* self) { else snprintf(buf, sizeof(buf), "%s, None", transform->id->name); - return PyString_FromString(buf); + return PyUnicode_FromString(buf); } // __repr__ method @@ -38,18 +38,18 @@ static PyObject* PyXmlSec_Transform__repr__(PyObject* self) { snprintf(buf, sizeof(buf), "__Transform('%s', '%s', %d)", transform->id->name, transform->id->href, transform->id->usage); else snprintf(buf, sizeof(buf), "__Transform('%s', None, %d)", transform->id->name, transform->id->usage); - return PyString_FromString(buf); + return PyUnicode_FromString(buf); } static const char PyXmlSec_TransformNameGet__doc__[] = "The transform's name."; static PyObject* PyXmlSec_TransformNameGet(PyXmlSec_Transform* self, void* closure) { - return PyString_FromString((const char*)self->id->name); + return PyUnicode_FromString((const char*)self->id->name); } static const char PyXmlSec_TransformHrefGet__doc__[] = "The transform's identification string (href)."; static PyObject* PyXmlSec_TransformHrefGet(PyXmlSec_Transform* self, void* closure) { if (self->id->href != NULL) - return PyString_FromString((const char*)self->id->href); + return PyUnicode_FromString((const char*)self->id->href); Py_RETURN_NONE; } @@ -149,7 +149,7 @@ static PyObject* PyXmlSec_KeyData__str__(PyObject* self) { snprintf(buf, sizeof(buf), "%s, %s", keydata->id->name, keydata->id->href); else snprintf(buf, sizeof(buf), "%s, None", keydata->id->name); - return PyString_FromString(buf); + return PyUnicode_FromString(buf); } // __repr__ method @@ -160,18 +160,18 @@ static PyObject* PyXmlSec_KeyData__repr__(PyObject* self) { snprintf(buf, sizeof(buf), "__KeyData('%s', '%s')", keydata->id->name, keydata->id->href); else snprintf(buf, sizeof(buf), "__KeyData('%s', None)", keydata->id->name); - return PyString_FromString(buf); + return PyUnicode_FromString(buf); } static const char PyXmlSec_KeyDataNameGet__doc__[] = "The key data's name."; static PyObject* PyXmlSec_KeyDataNameGet(PyXmlSec_KeyData* self, void* closure) { - return PyString_FromString((const char*)self->id->name); + return PyUnicode_FromString((const char*)self->id->name); } static const char PyXmlSec_KeyDataHrefGet__doc__[] = "The key data's identification string (href)."; static PyObject* PyXmlSec_KeyDataHrefGet(PyXmlSec_KeyData* self, void* closure) { if (self->id->href != NULL) - return PyString_FromString((const char*)self->id->href); + return PyUnicode_FromString((const char*)self->id->href); Py_RETURN_NONE; } @@ -245,7 +245,6 @@ static PyObject* PyXmlSec_KeyDataNew(xmlSecKeyDataId id) { return (PyObject*)keydata; } -#ifdef PY3K static PyModuleDef PyXmlSec_ConstantsModule = { PyModuleDef_HEAD_INIT, @@ -253,7 +252,6 @@ static PyModuleDef PyXmlSec_ConstantsModule = PYXMLSEC_CONSTANTS_DOC, -1, NULL, NULL, NULL, NULL, NULL }; -#endif // PY3K // initialize constants module and registers it base package int PyXmlSec_ConstantsModule_Init(PyObject* package) { @@ -267,12 +265,7 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PyObject* keyDataTypeCls = NULL; PyObject* tmp = NULL; -#ifdef PY3K constants = PyModule_Create(&PyXmlSec_ConstantsModule); -#else - constants = Py_InitModule3(STRINGIFY(MODULE_NAME) ".constants", NULL, PYXMLSEC_CONSTANTS_DOC); - Py_XINCREF(constants); -#endif if (!constants) return -1; @@ -292,7 +285,7 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { #undef PYXMLSEC_ADD_INT_CONSTANT #define PYXMLSEC_DECLARE_NAMESPACE(var, name) \ - if (!(var = PyCreateDummyObject(name))) goto ON_FAIL; \ + if (!(var = PyModule_New(name))) goto ON_FAIL; \ if (PyModule_AddObject(package, name, var) < 0) goto ON_FAIL; \ Py_INCREF(var); // add object steels reference @@ -308,7 +301,7 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { #define PYXMLSEC_ADD_NS_CONSTANT(name, lname) \ - tmp = PyString_FromString((const char*)(JOIN(xmlSec, name))); \ + tmp = PyUnicode_FromString((const char*)(JOIN(xmlSec, name))); \ PYXMLSEC_ADD_CONSTANT(nsCls, name, lname); // namespaces @@ -323,8 +316,6 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PYXMLSEC_ADD_NS_CONSTANT(XPathNs, "XPATH"); PYXMLSEC_ADD_NS_CONSTANT(XPath2Ns, "XPATH2"); PYXMLSEC_ADD_NS_CONSTANT(XPointerNs, "XPOINTER"); - PYXMLSEC_ADD_NS_CONSTANT(Soap11Ns, "SOAP11"); - PYXMLSEC_ADD_NS_CONSTANT(Soap12Ns, "SOAP12"); PYXMLSEC_ADD_NS_CONSTANT(NsExcC14N, "EXC_C14N"); PYXMLSEC_ADD_NS_CONSTANT(NsExcC14NWithComments, "EXC_C14N_WITH_COMMENT"); @@ -334,7 +325,7 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { #define PYXMLSEC_ADD_ENC_CONSTANT(name, lname) \ - tmp = PyString_FromString((const char*)(JOIN(xmlSec, name))); \ + tmp = PyUnicode_FromString((const char*)(JOIN(xmlSec, name))); \ PYXMLSEC_ADD_CONSTANT(encryptionTypeCls, name, lname); // encryption type @@ -349,7 +340,7 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { #define PYXMLSEC_ADD_NODE_CONSTANT(name, lname) \ - tmp = PyString_FromString((const char*)(JOIN(xmlSec, name))); \ + tmp = PyUnicode_FromString((const char*)(JOIN(xmlSec, name))); \ PYXMLSEC_ADD_CONSTANT(nodeCls, name, lname); // node @@ -448,12 +439,18 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataRetrievalMethod, "RETRIEVALMETHOD") PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataEncryptedKey, "ENCRYPTEDKEY") PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataAes, "AES") +#ifndef XMLSEC_NO_DES PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataDes, "DES") +#endif #ifndef XMLSEC_NO_DSA PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataDsa, "DSA") #endif -#if XMLSEC_VERSION_HEX > 306 +#if XMLSEC_VERSION_HEX > 0x10212 && XMLSEC_VERSION_HEX < 0x10303 + // from version 1.2.19 to version 1.3.2 (inclusive) PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataEcdsa, "ECDSA") +#elif XMLSEC_VERSION_HEX >= 0x10303 + // from version 1.3.3 (inclusive) + PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataEc, "ECDSA") #endif PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataHmac, "HMAC") PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataRsa, "RSA") @@ -484,7 +481,6 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformXPath, "XPATH"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformXPath2, "XPATH2"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformXPointer, "XPOINTER"); - PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformXslt, "XSLT"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRemoveXmlTagsC14N, "REMOVE_XML_TAGS_C14N"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformVisa3DHack, "VISA3D_HACK"); @@ -496,13 +492,19 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformKWAes192, "KW_AES192"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformKWAes256, "KW_AES256"); +#ifndef XMLSEC_NO_DES PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformDes3Cbc, "DES3"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformKWDes3, "KW_DES3"); +#endif #ifndef XMLSEC_NO_DSA PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformDsaSha1, "DSA_SHA1"); #endif +#ifndef XMLSEC_NO_XSLT + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformXslt, "XSLT"); +#endif -#if XMLSEC_VERSION_HEX > 306 +#if XMLSEC_VERSION_HEX > 0x10212 + // from version 1.2.19 PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformEcdsaSha1, "ECDSA_SHA1"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformEcdsaSha224, "ECDSA_SHA224"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformEcdsaSha256, "ECDSA_SHA256"); @@ -510,7 +512,10 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformEcdsaSha512, "ECDSA_SHA512"); #endif +#ifndef XMLSEC_NO_MD5 PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformHmacMd5, "HMAC_MD5"); +#endif + #ifndef XMLSEC_NO_RIPEMD160 PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformHmacRipemd160, "HMAC_RIPEMD160"); #endif @@ -520,7 +525,10 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformHmacSha384, "HMAC_SHA384"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformHmacSha512, "HMAC_SHA512"); +#ifndef XMLSEC_NO_MD5 PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRsaMd5, "RSA_MD5"); +#endif + #ifndef XMLSEC_NO_RIPEMD160 PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRsaRipemd160, "RSA_RIPEMD160"); #endif @@ -532,7 +540,10 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRsaPkcs1, "RSA_PKCS1"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRsaOaep, "RSA_OAEP"); +#ifndef XMLSEC_NO_MD5 PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformMd5, "MD5"); +#endif + #ifndef XMLSEC_NO_RIPEMD160 PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRipemd160, "RIPEMD160"); #endif @@ -543,6 +554,13 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformSha384, "SHA384"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformSha512, "SHA512"); +#if XMLSEC_VERSION_HEX > 0x1021B + // from version 1.2.28 + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformAes128Gcm, "AES128_GCM"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformAes192Gcm, "AES192_GCM"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformAes256Gcm, "AES256_GCM"); +#endif + PYXMLSEC_CLOSE_NAMESPACE(transformCls); #undef PYXMLSEC_ADD_TRANSFORM_CONSTANT diff --git a/src/ds.c b/src/ds.c index 43d3d118..d0b4bdf9 100644 --- a/src/ds.c +++ b/src/ds.c @@ -87,11 +87,21 @@ static int PyXmlSec_SignatureContextKeySet(PyObject* self, PyObject* value, void PyXmlSec_Key* key; PYXMLSEC_DEBUGF("%p, %p", self, value); + + if (value == NULL) { // key deletion + if (ctx->handle->signKey != NULL) { + xmlSecKeyDestroy(ctx->handle->signKey); + ctx->handle->signKey = NULL; + } + return 0; + } + if (!PyObject_IsInstance(value, (PyObject*)PyXmlSec_KeyType)) { PyErr_SetString(PyExc_TypeError, "instance of *xmlsec.Key* expected."); return -1; } key = (PyXmlSec_Key*)value; + if (key->handle == NULL) { PyErr_SetString(PyExc_TypeError, "empty key."); return -1; @@ -252,6 +262,7 @@ static int PyXmlSec_ProcessSignBinary(PyXmlSec_SignatureContext* ctx, const xmlS if (ctx->handle->signKey == NULL) { PyErr_SetString(PyXmlSec_Error, "Sign key is not specified."); + return -1; } if (ctx->handle->signMethod != NULL) { diff --git a/src/enc.c b/src/enc.c index 02c01d8a..42195dd3 100644 --- a/src/enc.c +++ b/src/enc.c @@ -17,6 +17,11 @@ #include #include +// Backwards compatibility with xmlsec 1.2 +#ifndef XMLSEC_KEYINFO_FLAGS_LAX_KEY_SEARCH +#define XMLSEC_KEYINFO_FLAGS_LAX_KEY_SEARCH 0x00008000 +#endif + typedef struct { PyObject_HEAD xmlSecEncCtxPtr handle; @@ -50,6 +55,13 @@ static int PyXmlSec_EncryptionContext__init__(PyObject* self, PyObject* args, Py } ctx->manager = manager; PYXMLSEC_DEBUGF("%p: init enc context - ok, manager - %p", self, manager); + + // xmlsec 1.3 changed the key search to strict mode, causing various examples + // in the docs to fail. For backwards compatibility, this changes it back to + // lax mode for now. + ctx->handle->keyInfoReadCtx.flags = XMLSEC_KEYINFO_FLAGS_LAX_KEY_SEARCH; + ctx->handle->keyInfoWriteCtx.flags = XMLSEC_KEYINFO_FLAGS_LAX_KEY_SEARCH; + return 0; ON_FAIL: PYXMLSEC_DEBUGF("%p: init enc context - failed", self); @@ -90,6 +102,15 @@ static int PyXmlSec_EncryptionContextKeySet(PyObject* self, PyObject* value, voi PyXmlSec_Key* key; PYXMLSEC_DEBUGF("%p, %p", self, value); + + if (value == NULL) { // key deletion + if (ctx->handle->encKey != NULL) { + xmlSecKeyDestroy(ctx->handle->encKey); + ctx->handle->encKey = NULL; + } + return 0; + } + if (!PyObject_IsInstance(value, (PyObject*)PyXmlSec_KeyType)) { PyErr_SetString(PyExc_TypeError, "instance of *xmlsec.Key* expected."); return -1; @@ -174,7 +195,7 @@ static PyObject* PyXmlSec_EncryptionContextEncryptBinary(PyObject* self, PyObjec // release the replaced nodes in a way safe for `lxml` static void PyXmlSec_ClearReplacedNodes(xmlSecEncCtxPtr ctx, PyXmlSec_LxmlDocumentPtr doc) { - PyXmlSec_LxmlElementPtr* elem; + PyXmlSec_LxmlElementPtr elem; // release the replaced nodes in a way safe for `lxml` xmlNodePtr n = ctx->replacedNodeList; xmlNodePtr nn; @@ -183,7 +204,7 @@ static void PyXmlSec_ClearReplacedNodes(xmlSecEncCtxPtr ctx, PyXmlSec_LxmlDocume PYXMLSEC_DEBUGF("clear replaced node %p", n); nn = n->next; // if n has references, it will not be deleted - elem = PyXmlSec_elementFactory(doc, n); + elem = (PyXmlSec_LxmlElementPtr)PyXmlSec_elementFactory(doc, n); if (NULL == elem) xmlFreeNode(n); else @@ -224,7 +245,7 @@ static PyObject* PyXmlSec_EncryptionContextEncryptXml(PyObject* self, PyObject* } tmpType = xmlGetProp(template->_c_node, XSTR("Type")); if (tmpType == NULL || !(xmlStrEqual(tmpType, xmlSecTypeEncElement) || xmlStrEqual(tmpType, xmlSecTypeEncContent))) { - PyErr_SetString(PyXmlSec_Error, "unsupported `Type`, it should be `element` or `content`)"); + PyErr_SetString(PyXmlSec_Error, "unsupported `Type`, it should be `element` or `content`"); goto ON_FAIL; } diff --git a/src/exception.c b/src/exception.c index 2ca5ab57..ac0e44ee 100644 --- a/src/exception.c +++ b/src/exception.c @@ -23,7 +23,11 @@ PyObject* PyXmlSec_Error; PyObject* PyXmlSec_InternalError; PyObject* PyXmlSec_VerificationError; +#if PY_MINOR_VERSION >= 7 +static Py_tss_t PyXmlSec_LastErrorKey; +#else static int PyXmlSec_LastErrorKey = 0; +#endif static int PyXmlSec_PrintErrorMessage = 0; @@ -71,16 +75,26 @@ static PyXmlSec_ErrorHolder* PyXmlSec_ExchangeLastError(PyXmlSec_ErrorHolder* e) PyXmlSec_ErrorHolder* v; int r; + #if PY_MINOR_VERSION >= 7 + if (PyThread_tss_is_created(&PyXmlSec_LastErrorKey) == 0) { + #else if (PyXmlSec_LastErrorKey == 0) { + #endif PYXMLSEC_DEBUG("WARNING: There is no error key."); PyXmlSec_ErrorHolderFree(e); return NULL; } // get_key_value and set_key_value are gil free + #if PY_MINOR_VERSION >= 7 + v = (PyXmlSec_ErrorHolder*)PyThread_tss_get(&PyXmlSec_LastErrorKey); + //PyThread_tss_delete(&PyXmlSec_LastErrorKey); + r = PyThread_tss_set(&PyXmlSec_LastErrorKey, (void*)e); + #else v = (PyXmlSec_ErrorHolder*)PyThread_get_key_value(PyXmlSec_LastErrorKey); PyThread_delete_key_value(PyXmlSec_LastErrorKey); r = PyThread_set_key_value(PyXmlSec_LastErrorKey, (void*)e); + #endif PYXMLSEC_DEBUGF("set_key_value returns %d", r); return v; } @@ -165,6 +179,16 @@ void PyXmlSecEnableDebugTrace(int v) { PyXmlSec_PrintErrorMessage = v; } +void PyXmlSec_InstallErrorCallback() { + #if PY_MINOR_VERSION >= 7 + if (PyThread_tss_is_created(&PyXmlSec_LastErrorKey) != 0) { + #else + if (PyXmlSec_LastErrorKey != 0) { + #endif + xmlSecErrorsSetCallback(PyXmlSec_ErrorCallback); + } +} + // initializes errors module int PyXmlSec_ExceptionsModule_Init(PyObject* package) { PyXmlSec_Error = NULL; @@ -184,10 +208,14 @@ int PyXmlSec_ExceptionsModule_Init(PyObject* package) { if (PyModule_AddObject(package, "InternalError", PyXmlSec_InternalError) < 0) goto ON_FAIL; if (PyModule_AddObject(package, "VerificationError", PyXmlSec_VerificationError) < 0) goto ON_FAIL; - PyXmlSec_LastErrorKey = PyThread_create_key(); - if (PyXmlSec_LastErrorKey != 0) { - xmlSecErrorsSetCallback(&PyXmlSec_ErrorCallback); + #if PY_MINOR_VERSION >= 7 + if (PyThread_tss_create(&PyXmlSec_LastErrorKey) == 0) { + PyXmlSec_InstallErrorCallback(); } + #else + PyXmlSec_LastErrorKey = PyThread_create_key(); + PyXmlSec_InstallErrorCallback(); + #endif return 0; diff --git a/src/exception.h b/src/exception.h index 9dea5ecb..687cd778 100644 --- a/src/exception.h +++ b/src/exception.h @@ -24,4 +24,6 @@ void PyXmlSec_ClearError(void); void PyXmlSecEnableDebugTrace(int); +void PyXmlSec_InstallErrorCallback(); + #endif //__PYXMLSEC_EXCEPTIONS_H__ diff --git a/src/keys.c b/src/keys.c index 7fd080a1..5ff04aae 100644 --- a/src/keys.c +++ b/src/keys.c @@ -142,7 +142,7 @@ static PyObject* PyXmlSec_KeyFromFile(PyObject* self, PyObject* args, PyObject* Py_ssize_t data_size = 0; PYXMLSEC_DEBUG("load key from file - start"); - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OH|z:from_file", kwlist, &file, &format, &password)) { + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OI|z:from_file", kwlist, &file, &format, &password)) { goto ON_FAIL; } @@ -163,7 +163,12 @@ static PyObject* PyXmlSec_KeyFromFile(PyObject* self, PyObject* args, PyObject* if (is_content) { key->handle = xmlSecCryptoAppKeyLoadMemory((const xmlSecByte*)data, (xmlSecSize)data_size, format, password, NULL, NULL); } else { - key->handle = xmlSecCryptoAppKeyLoad(data, format, password, NULL, NULL); + #if XMLSEC_VERSION_HEX >= 0x10303 + // from version 1.3.3 (inclusive) + key->handle = xmlSecCryptoAppKeyLoadEx(data, xmlSecKeyDataTypePrivate, format, password, NULL, NULL); + #else + key->handle = xmlSecCryptoAppKeyLoad(data, format, password, NULL, NULL); + #endif } Py_END_ALLOW_THREADS; @@ -185,6 +190,51 @@ static PyObject* PyXmlSec_KeyFromFile(PyObject* self, PyObject* args, PyObject* return NULL; } +static const char PyXmlSec_KeyFromEngine__doc__[] = \ + "from_engine(engine_and_key_id) -> xmlsec.Key\n" + "Loads PKI key from an engine.\n\n" + ":param engine_and_key_id: engine and key id, i.e. 'pkcs11;pkcs11:token=XmlsecToken;object=XmlsecKey;pin-value=password'\n" + ":type engine_and_key_id: :class:`str`, " + ":return: pointer to newly created key\n" + ":rtype: :class:`~xmlsec.Key`"; +static PyObject* PyXmlSec_KeyFromEngine(PyObject* self, PyObject* args, PyObject* kwargs) { + static char *kwlist[] = {"engine_and_key_id", NULL}; + + const char* engine_and_key_id = NULL; + PyXmlSec_Key* key = NULL; + + PYXMLSEC_DEBUG("load key from engine - start"); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s:from_engine", kwlist, &engine_and_key_id)) { + goto ON_FAIL; + } + + if ((key = PyXmlSec_NewKey1((PyTypeObject*)self)) == NULL) goto ON_FAIL; + + Py_BEGIN_ALLOW_THREADS; + #if XMLSEC_VERSION_HEX >= 0x10303 + // from version 1.3.3 (inclusive) + key->handle = xmlSecCryptoAppKeyLoadEx(engine_and_key_id, xmlSecKeyDataTypePrivate, xmlSecKeyDataFormatEngine, NULL, xmlSecCryptoAppGetDefaultPwdCallback(), (void*)engine_and_key_id); + #else + key->handle = xmlSecCryptoAppKeyLoad(engine_and_key_id, xmlSecKeyDataFormatEngine, NULL, xmlSecCryptoAppGetDefaultPwdCallback(), (void*)engine_and_key_id); + #endif + Py_END_ALLOW_THREADS; + + if (key->handle == NULL) { + PyXmlSec_SetLastError("cannot read key"); + goto ON_FAIL; + } + + key->is_own = 1; + + PYXMLSEC_DEBUG("load key from engine - ok"); + return (PyObject*)key; + +ON_FAIL: + PYXMLSEC_DEBUG("load key from engine - fail"); + Py_XDECREF(key); + return NULL; +} + static const char PyXmlSec_KeyGenerate__doc__[] = \ "generate(klass, size, type) -> xmlsec.Key\n" "Generates key of kind ``klass`` with ``size`` and ``type``.\n\n" @@ -249,7 +299,7 @@ static PyObject* PyXmlSec_KeyFromBinaryFile(PyObject* self, PyObject* args, PyOb PYXMLSEC_DEBUG("load symmetric key - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!O&:from_binary_file", kwlist, - PyXmlSec_KeyDataType, &keydata, PyString_FSConverter, &filepath)) + PyXmlSec_KeyDataType, &keydata, PyUnicode_FSConverter, &filepath)) { goto ON_FAIL; } @@ -436,7 +486,7 @@ static PyObject* PyXmlSec_KeyNameGet(PyObject* self, void* closure) { } cname = (const char*)xmlSecKeyGetName(key->handle); if (cname != NULL) { - return PyString_FromString(cname); + return PyUnicode_FromString(cname); } Py_RETURN_NONE; } @@ -452,10 +502,21 @@ static int PyXmlSec_KeyNameSet(PyObject* self, PyObject* value, void* closure) { return -1; } - name = PyString_AsString(value); + if (value == NULL) { + if (xmlSecKeySetName(key->handle, NULL) < 0) { + PyXmlSec_SetLastError("cannot delete name"); + return -1; + } + return 0; + } + + name = PyUnicode_AsUTF8(value); if (name == NULL) return -1; - xmlSecKeySetName(key->handle, XSTR(name)); + if (xmlSecKeySetName(key->handle, XSTR(name)) < 0) { + PyXmlSec_SetLastError("cannot set name"); + return -1; + } return 0; } @@ -483,6 +544,12 @@ static PyMethodDef PyXmlSec_KeyMethods[] = { METH_CLASS|METH_VARARGS|METH_KEYWORDS, PyXmlSec_KeyFromFile__doc__ }, + { + "from_engine", + (PyCFunction)PyXmlSec_KeyFromEngine, + METH_CLASS|METH_VARARGS|METH_KEYWORDS, + PyXmlSec_KeyFromEngine__doc__ + }, { "generate", (PyCFunction)PyXmlSec_KeyGenerate, @@ -687,7 +754,7 @@ static PyObject* PyXmlSec_KeysManagerLoadCert(PyObject* self, PyObject* args, Py PYXMLSEC_DEBUGF("%p: load cert - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&II:load_cert", kwlist, - PyString_FSConverter, &filepath, &format, &type)) { + PyUnicode_FSConverter, &filepath, &format, &type)) { goto ON_FAIL; } diff --git a/src/lxml.c b/src/lxml.c index 862ee682..c98e933b 100644 --- a/src/lxml.c +++ b/src/lxml.c @@ -9,16 +9,104 @@ #include "common.h" #include "lxml.h" +#include "exception.h" #include -#include +#include #include #include #include +#define XMLSEC_EXTRACT_VERSION(x, y) ((x / (y)) % 100) + +#define XMLSEC_EXTRACT_MAJOR(x) XMLSEC_EXTRACT_VERSION(x, 100 * 100) +#define XMLSEC_EXTRACT_MINOR(x) XMLSEC_EXTRACT_VERSION(x, 100) +#define XMLSEC_EXTRACT_PATCH(x) XMLSEC_EXTRACT_VERSION(x, 1) + +static long PyXmlSec_GetLibXmlVersionLong() { + return PyOS_strtol(xmlParserVersion, NULL, 10); +} +long PyXmlSec_GetLibXmlVersionMajor() { + return XMLSEC_EXTRACT_MAJOR(PyXmlSec_GetLibXmlVersionLong()); +} +long PyXmlSec_GetLibXmlVersionMinor() { + return XMLSEC_EXTRACT_MINOR(PyXmlSec_GetLibXmlVersionLong()); +} +long PyXmlSec_GetLibXmlVersionPatch() { + return XMLSEC_EXTRACT_PATCH(PyXmlSec_GetLibXmlVersionLong()); +} + +long PyXmlSec_GetLibXmlCompiledVersionMajor() { + return XMLSEC_EXTRACT_MAJOR(LIBXML_VERSION); +} +long PyXmlSec_GetLibXmlCompiledVersionMinor() { + return XMLSEC_EXTRACT_MINOR(LIBXML_VERSION); +} +long PyXmlSec_GetLibXmlCompiledVersionPatch() { + return XMLSEC_EXTRACT_PATCH(LIBXML_VERSION); +} + +static int PyXmlSec_CheckLxmlLibraryVersion(void) { + // Make sure that the version of libxml2 lxml is using is the same as the one we are using. Because + // we pass trees between the two libraries, we need to make sure that they are using the same version + // of libxml2, or we could run into difficult to debug segfaults. + // See: https://github.com/xmlsec/python-xmlsec/issues/283 + + PyObject* lxml = NULL; + PyObject* version = NULL; + + // Default to failure + int result = -1; + + lxml = PyImport_ImportModule("lxml.etree"); + if (lxml == NULL) { + goto FINALIZE; + } + version = PyObject_GetAttrString(lxml, "LIBXML_VERSION"); + if (version == NULL) { + goto FINALIZE; + } + if (!PyTuple_Check(version) || PyTuple_Size(version) < 2) { + goto FINALIZE; + } + + PyObject* major = PyTuple_GetItem(version, 0); + if (major == NULL) { + goto FINALIZE; + } + PyObject* minor = PyTuple_GetItem(version, 1); + if (minor == NULL) { + goto FINALIZE; + } + + if (!PyLong_Check(major) || !PyLong_Check(minor)) { + goto FINALIZE; + } + + if (PyLong_AsLong(major) != PyXmlSec_GetLibXmlVersionMajor() || PyLong_AsLong(minor) != PyXmlSec_GetLibXmlVersionMinor()) { + goto FINALIZE; + } + + result = 0; + +FINALIZE: + // Clear any errors that may have occurred + PyErr_Clear(); + + // Cleanup our references, and return the result + Py_XDECREF(lxml); + Py_XDECREF(version); + + return result; +} int PyXmlSec_InitLxmlModule(void) { + if (PyXmlSec_CheckLxmlLibraryVersion() < 0) { + PyXmlSec_SetLastError("lxml & xmlsec libxml2 library version mismatch"); + return -1; + } + return import_lxml__etree(); } diff --git a/src/lxml.h b/src/lxml.h index 435ccdff..72050efe 100644 --- a/src/lxml.h +++ b/src/lxml.h @@ -16,7 +16,7 @@ #include #include -#include +#include typedef struct LxmlElement* PyXmlSec_LxmlElementPtr; typedef struct LxmlDocument* PyXmlSec_LxmlDocumentPtr; @@ -29,4 +29,13 @@ PyXmlSec_LxmlElementPtr PyXmlSec_elementFactory(PyXmlSec_LxmlDocumentPtr doc, xm // converts o to PyObject, None object is not allowed, does not increment ref_counts int PyXmlSec_LxmlElementConverter(PyObject* o, PyXmlSec_LxmlElementPtr* p); +// get version numbers for libxml2 both compiled and loaded +long PyXmlSec_GetLibXmlVersionMajor(); +long PyXmlSec_GetLibXmlVersionMinor(); +long PyXmlSec_GetLibXmlVersionPatch(); + +long PyXmlSec_GetLibXmlCompiledVersionMajor(); +long PyXmlSec_GetLibXmlCompiledVersionMinor(); +long PyXmlSec_GetLibXmlCompiledVersionPatch(); + #endif // __PYXMLSEC_LXML_H__ diff --git a/src/main.c b/src/main.c index 85f457f2..61eac139 100644 --- a/src/main.c +++ b/src/main.c @@ -10,10 +10,13 @@ #include "common.h" #include "platform.h" #include "exception.h" +#include "lxml.h" #include #include #include +#include +#include #define _PYXMLSEC_FREE_NONE 0 #define _PYXMLSEC_FREE_XMLSEC 1 @@ -26,7 +29,7 @@ static int free_mode = _PYXMLSEC_FREE_NONE; #ifndef XMLSEC_NO_CRYPTO_DYNAMIC_LOADING static const xmlChar* PyXmlSec_GetCryptoLibName() { -#if XMLSEC_VERSION_HEX > 308 +#if XMLSEC_VERSION_HEX > 0x10214 // xmlSecGetDefaultCrypto was introduced in version 1.2.21 const xmlChar* cryptoLib = xmlSecGetDefaultCrypto(); #else @@ -86,6 +89,11 @@ static int PyXmlSec_Init(void) { PyXmlSec_Free(_PYXMLSEC_FREE_ALL); return -1; } + // xmlsec will install default callback in xmlSecCryptoInit, + // overwriting any custom callbacks. + // We thus reinstall our callback now. + PyXmlSec_InstallErrorCallback(); + free_mode = _PYXMLSEC_FREE_ALL; return 0; } @@ -112,6 +120,37 @@ static PyObject* PyXmlSec_PyShutdown(PyObject* self) { Py_RETURN_NONE; } +static char PyXmlSec_GetLibXmlSecVersion__doc__[] = \ + "get_libxmlsec_version() -> tuple\n" + "Returns Version tuple of wrapped libxmlsec library."; +static PyObject* PyXmlSec_GetLibXmlSecVersion() { + return Py_BuildValue("(iii)", XMLSEC_VERSION_MAJOR, XMLSEC_VERSION_MINOR, XMLSEC_VERSION_SUBMINOR); +} + +static char PyXmlSec_GetLibXmlVersion__doc__[] = \ + "get_libxml_version() -> tuple[int, int, int]\n" + "Returns version tuple of libxml2 library xmlsec is using."; +static PyObject* PyXmlSec_GetLibXmlVersion() { + return Py_BuildValue( + "(iii)", + PyXmlSec_GetLibXmlVersionMajor(), + PyXmlSec_GetLibXmlVersionMinor(), + PyXmlSec_GetLibXmlVersionPatch() + ); +} + +static char PyXmlSec_GetLibXmlCompiledVersion__doc__[] = \ + "get_libxml_compiled_version() -> tuple[int, int, int]\n" + "Returns version tuple of libxml2 library xmlsec was compiled with."; +static PyObject* PyXmlSec_GetLibXmlCompiledVersion() { + return Py_BuildValue( + "(iii)", + PyXmlSec_GetLibXmlCompiledVersionMajor(), + PyXmlSec_GetLibXmlCompiledVersionMinor(), + PyXmlSec_GetLibXmlCompiledVersionPatch() + ); +} + static char PyXmlSec_PyEnableDebugOutput__doc__[] = \ "enable_debug_trace(enabled) -> None\n" "Enables or disables calling LibXML2 callback from the default errors callback.\n\n" @@ -127,6 +166,245 @@ static PyObject* PyXmlSec_PyEnableDebugOutput(PyObject *self, PyObject* args, Py Py_RETURN_NONE; } +// NB: This whole thing assumes that the `xmlsec` callbacks are not re-entrant +// (i.e. that xmlsec won't come across a link in the reference it's processing +// and try to open that with these callbacks too). +typedef struct CbList { + PyObject* match_cb; + PyObject* open_cb; + PyObject* read_cb; + PyObject* close_cb; + struct CbList* next; +} CbList; + +static CbList* registered_callbacks = NULL; + +static void RCBListCons(CbList* cb_list_item) { + cb_list_item->next = registered_callbacks; + registered_callbacks = cb_list_item; +} + +static void RCBListClear() { + CbList* cb_list_item = registered_callbacks; + while (cb_list_item) { + Py_DECREF(cb_list_item->match_cb); + Py_DECREF(cb_list_item->open_cb); + Py_DECREF(cb_list_item->read_cb); + Py_DECREF(cb_list_item->close_cb); + CbList* next = cb_list_item->next; + free(cb_list_item); + cb_list_item = next; + } + registered_callbacks = NULL; +} + +// The currently executing set of Python callbacks: +static CbList* cur_cb_list_item; + +static int PyXmlSec_MatchCB(const char* filename) { + cur_cb_list_item = registered_callbacks; + PyGILState_STATE state = PyGILState_Ensure(); + PyObject* args = Py_BuildValue("(y)", filename); + while (cur_cb_list_item) { + PyObject* result = PyObject_CallObject(cur_cb_list_item->match_cb, args); + if (result && PyObject_IsTrue(result)) { + Py_DECREF(result); + Py_DECREF(args); + PyGILState_Release(state); + return 1; + } + Py_XDECREF(result); + cur_cb_list_item = cur_cb_list_item->next; + } + Py_DECREF(args); + PyGILState_Release(state); + return 0; +} + +static void* PyXmlSec_OpenCB(const char* filename) { + PyGILState_STATE state = PyGILState_Ensure(); + + // NB: Assumes the match callback left the current callback list item in the + // right place: + PyObject* args = Py_BuildValue("(y)", filename); + PyObject* result = PyObject_CallObject(cur_cb_list_item->open_cb, args); + Py_DECREF(args); + + PyGILState_Release(state); + return result; +} + +static int PyXmlSec_ReadCB(void* context, char* buffer, int len) { + PyGILState_STATE state = PyGILState_Ensure(); + + // NB: Assumes the match callback left the current callback list item in the + // right place: + PyObject* py_buffer = PyMemoryView_FromMemory(buffer, (Py_ssize_t) len, PyBUF_WRITE); + PyObject* args = Py_BuildValue("(OO)", context, py_buffer); + PyObject* py_bytes_read = PyObject_CallObject(cur_cb_list_item->read_cb, args); + Py_DECREF(args); + Py_DECREF(py_buffer); + int result; + if (py_bytes_read && PyLong_Check(py_bytes_read)) { + result = (int)PyLong_AsLong(py_bytes_read); + } else { + result = EOF; + } + Py_XDECREF(py_bytes_read); + + PyGILState_Release(state); + return result; +} + +static int PyXmlSec_CloseCB(void* context) { + PyGILState_STATE state = PyGILState_Ensure(); + + PyObject* args = Py_BuildValue("(O)", context); + PyObject* result = PyObject_CallObject(cur_cb_list_item->close_cb, args); + Py_DECREF(args); + Py_DECREF(context); + Py_DECREF(result); + + PyGILState_Release(state); + return 0; +} + +static char PyXmlSec_PyIOCleanupCallbacks__doc__[] = \ + "Unregister globally all sets of IO callbacks from xmlsec."; +static PyObject* PyXmlSec_PyIOCleanupCallbacks(PyObject *self) { + xmlSecIOCleanupCallbacks(); + // We always have callbacks registered to delegate to any Python callbacks + // we have registered within these bindings: + if (xmlSecIORegisterCallbacks( + PyXmlSec_MatchCB, PyXmlSec_OpenCB, PyXmlSec_ReadCB, + PyXmlSec_CloseCB) < 0) { + return NULL; + } + RCBListClear(); + Py_RETURN_NONE; +} + +static char PyXmlSec_PyIORegisterDefaultCallbacks__doc__[] = \ + "Register globally xmlsec's own default set of IO callbacks."; +static PyObject* PyXmlSec_PyIORegisterDefaultCallbacks(PyObject *self) { + // NB: The default callbacks (specifically libxml2's `xmlFileMatch`) always + // match, and callbacks are called in the reverse order to that which they + // were added. So, there's no point in holding onto any previously registered + // callbacks, because they will never be run: + xmlSecIOCleanupCallbacks(); + RCBListClear(); + if (xmlSecIORegisterDefaultCallbacks() < 0) { + return NULL; + } + // We need to make sure we can continue trying to match any newly added + // Python callbacks: + if (xmlSecIORegisterCallbacks( + PyXmlSec_MatchCB, PyXmlSec_OpenCB, PyXmlSec_ReadCB, + PyXmlSec_CloseCB) < 0) { + return NULL; + }; + Py_RETURN_NONE; +} + +static char PyXmlSec_PyIORegisterCallbacks__doc__[] = \ + "register_callbacks(input_match_callback, input_open_callback, input_read_callback, input_close_callback) -> None\n" + "Register globally a custom set of IO callbacks with xmlsec.\n\n" + ":param input_match_callback: A callable that takes a filename `bytestring` and " + "returns a boolean as to whether the other callbacks in this set can handle that name.\n" + ":type input_match_callback: ~collections.abc.Callable[[bytes], bool]\n" + ":param input_open_callback: A callable that takes a filename and returns some " + "context object (e.g. a file object) that the remaining callables in this set will be passed " + "during handling.\n" + ":type input_open_callback: ~collections.abc.Callable[[bytes], Any]\n" + // FIXME: How do we handle failures in ^^ (e.g. can't find the file)? + ":param input_read_callback: A callable that that takes the context object from the " + "open callback and a buffer, and should fill the buffer with data (e.g. BytesIO.readinto()). " + "xmlsec will call this function several times until there is no more data returned.\n" + ":type input_read_callback: ~collections.abc.Callable[[Any, memoryview], int]\n" + ":param input_close_callback: A callable that takes the context object from the " + "open callback and can do any resource cleanup necessary.\n" + ":type input_close_callback: ~collections.abc.Callable[[Any], None]\n" + ; +static PyObject* PyXmlSec_PyIORegisterCallbacks(PyObject *self, PyObject *args, PyObject *kwargs) { + static char *kwlist[] = { + "input_match_callback", + "input_open_callback", + "input_read_callback", + "input_close_callback", + NULL + }; + CbList* cb_list_item = malloc(sizeof(CbList)); + if (cb_list_item == NULL) { + return NULL; + } + if (!PyArg_ParseTupleAndKeywords( + args, kwargs, "OOOO:register_callbacks", kwlist, + &cb_list_item->match_cb, &cb_list_item->open_cb, &cb_list_item->read_cb, + &cb_list_item->close_cb)) { + free(cb_list_item); + return NULL; + } + if (!PyCallable_Check(cb_list_item->match_cb)) { + PyErr_SetString(PyExc_TypeError, "input_match_callback must be a callable"); + free(cb_list_item); + return NULL; + } + if (!PyCallable_Check(cb_list_item->open_cb)) { + PyErr_SetString(PyExc_TypeError, "input_open_callback must be a callable"); + free(cb_list_item); + return NULL; + } + if (!PyCallable_Check(cb_list_item->read_cb)) { + PyErr_SetString(PyExc_TypeError, "input_read_callback must be a callable"); + free(cb_list_item); + return NULL; + } + if (!PyCallable_Check(cb_list_item->close_cb)) { + PyErr_SetString(PyExc_TypeError, "input_close_callback must be a callable"); + free(cb_list_item); + return NULL; + } + Py_INCREF(cb_list_item->match_cb); + Py_INCREF(cb_list_item->open_cb); + Py_INCREF(cb_list_item->read_cb); + Py_INCREF(cb_list_item->close_cb); + cb_list_item->next = NULL; + RCBListCons(cb_list_item); + // NB: We don't need to register the callbacks with `xmlsec` here, because + // we've already registered our helper functions that will trawl through our + // list of callbacks. + Py_RETURN_NONE; +} + +static char PyXmlSec_PyBase64DefaultLineSize__doc__[] = \ + "base64_default_line_size(size = None)\n" + "Configures the default maximum columns size for base64 encoding.\n\n" + "If ``size`` is not given, this function returns the current default size, acting as a getter. " + "If ``size`` is given, a new value is applied and this function returns nothing, acting as a setter.\n" + ":param size: new default size value (optional)\n" + ":type size: :class:`int` or :data:`None`"; +static PyObject* PyXmlSec_PyBase64DefaultLineSize(PyObject *self, PyObject *args, PyObject *kwargs) { + static char *kwlist[] = { "size", NULL }; + PyObject *pySize = NULL; + int size; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O:base64_default_line_size", kwlist, &pySize)) { + return NULL; + } + if (pySize == NULL) { + return PyLong_FromLong(xmlSecBase64GetDefaultLineSize()); + } + size = (int)PyLong_AsLong(pySize); + if (PyErr_Occurred()) { + return NULL; + } + if (size < 0) { + PyErr_SetString(PyExc_ValueError, "size must be positive"); + return NULL; + } + xmlSecBase64SetDefaultLineSize(size); + Py_RETURN_NONE; +} + static PyMethodDef PyXmlSec_MainMethods[] = { { "init", @@ -140,12 +418,54 @@ static PyMethodDef PyXmlSec_MainMethods[] = { METH_NOARGS, PyXmlSec_PyShutdown__doc__ }, + { + "get_libxmlsec_version", + (PyCFunction)PyXmlSec_GetLibXmlSecVersion, + METH_NOARGS, + PyXmlSec_GetLibXmlSecVersion__doc__ + }, + { + "get_libxml_version", + (PyCFunction)PyXmlSec_GetLibXmlVersion, + METH_NOARGS, + PyXmlSec_GetLibXmlVersion__doc__ + }, + { + "get_libxml_compiled_version", + (PyCFunction)PyXmlSec_GetLibXmlCompiledVersion, + METH_NOARGS, + PyXmlSec_GetLibXmlCompiledVersion__doc__ + }, { "enable_debug_trace", (PyCFunction)PyXmlSec_PyEnableDebugOutput, METH_VARARGS|METH_KEYWORDS, PyXmlSec_PyEnableDebugOutput__doc__ }, + { + "cleanup_callbacks", + (PyCFunction)PyXmlSec_PyIOCleanupCallbacks, + METH_NOARGS, + PyXmlSec_PyIOCleanupCallbacks__doc__ + }, + { + "register_default_callbacks", + (PyCFunction)PyXmlSec_PyIORegisterDefaultCallbacks, + METH_NOARGS, + PyXmlSec_PyIORegisterDefaultCallbacks__doc__ + }, + { + "register_callbacks", + (PyCFunction)PyXmlSec_PyIORegisterCallbacks, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_PyIORegisterCallbacks__doc__ + }, + { + "base64_default_line_size", + (PyCFunction)PyXmlSec_PyBase64DefaultLineSize, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_PyBase64DefaultLineSize__doc__ + }, {NULL, NULL} /* sentinel */ }; @@ -167,8 +487,6 @@ int PyXmlSec_EncModule_Init(PyObject* package); // templates management int PyXmlSec_TemplateModule_Init(PyObject* package); -#ifdef PY3K - static int PyXmlSec_PyClear(PyObject *self) { PyXmlSec_Free(free_mode); return 0; @@ -189,54 +507,12 @@ static PyModuleDef PyXmlSecModule = { #define PYENTRY_FUNC_NAME JOIN(PyInit_, MODULE_NAME) #define PY_MOD_RETURN(m) return m -#else // PY3K -#define PYENTRY_FUNC_NAME JOIN(init, MODULE_NAME) -#define PY_MOD_RETURN(m) return - -static void PyXmlSec_PyModuleGuard__del__(PyObject* self) -{ - PyXmlSec_Free(free_mode); - Py_TYPE(self)->tp_free(self); -} - -// we need guard to free resources on module unload -typedef struct { - PyObject_HEAD -} PyXmlSec_PyModuleGuard; - -static PyTypeObject PyXmlSec_PyModuleGuardType = { - PyVarObject_HEAD_INIT(NULL, 0) - STRINGIFY(MODULE_NAME) "__Guard", /* tp_name */ - sizeof(PyXmlSec_PyModuleGuard), /* tp_basicsize */ - 0, /* tp_itemsize */ - PyXmlSec_PyModuleGuard__del__, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_reserved */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ -}; -#endif // PY3K PyMODINIT_FUNC PYENTRY_FUNC_NAME(void) { PyObject *module = NULL; -#ifdef PY3K module = PyModule_Create(&PyXmlSecModule); -#else - module = Py_InitModule3(STRINGIFY(MODULE_NAME), PyXmlSec_MainMethods, MODULE_DOC); -#endif if (!module) { PY_MOD_RETURN(NULL); /* this really should never happen */ } @@ -258,13 +534,6 @@ PYENTRY_FUNC_NAME(void) if (PyXmlSec_EncModule_Init(module) < 0) goto ON_FAIL; if (PyXmlSec_TemplateModule_Init(module) < 0) goto ON_FAIL; -#ifndef PY3K - if (PyType_Ready(&PyXmlSec_PyModuleGuardType) < 0) goto ON_FAIL; - PYXMLSEC_DEBUGF("%p", &PyXmlSec_PyModuleGuardType); - // added guard to free resources on module unload, this should be called after last - if (PyModule_AddObject(module, "__guard", _PyObject_New(&PyXmlSec_PyModuleGuardType)) < 0) goto ON_FAIL; -#endif - PY_MOD_RETURN(module); ON_FAIL: PY_MOD_RETURN(NULL); diff --git a/src/platform.h b/src/platform.h index 795062f2..35163e88 100644 --- a/src/platform.h +++ b/src/platform.h @@ -19,11 +19,11 @@ #include #endif /* MS_WIN32 */ -#define XMLSEC_VERSION_HEX ((XMLSEC_VERSION_MAJOR << 8) | (XMLSEC_VERSION_MINOR << 4) | (XMLSEC_VERSION_SUBMINOR)) +#define XMLSEC_VERSION_HEX ((XMLSEC_VERSION_MAJOR << 16) | (XMLSEC_VERSION_MINOR << 8) | (XMLSEC_VERSION_SUBMINOR)) // XKMS support was removed in version 1.2.21 // https://mail.gnome.org/archives/commits-list/2015-February/msg10555.html -#if XMLSEC_VERSION_HEX > 0x134 +#if XMLSEC_VERSION_HEX > 0x10214 #define XMLSEC_NO_XKMS 1 #endif @@ -35,50 +35,6 @@ typedef int Py_ssize_t; #define PY_SSIZE_T_MIN INT_MIN #endif -#if PY_MAJOR_VERSION >= 3 -#define PY3K 1 -#define PyString_Check PyUnicode_Check -#define PyString_FromStringAndSize PyUnicode_FromStringAndSize - -#define PyString_FromString PyUnicode_FromString - -#define PyString_AsString PyUnicode_AsUTF8 -#define PyString_AsUtf8AndSize PyUnicode_AsUTF8AndSize - -#define PyCreateDummyObject PyModule_New - -#define PyString_FSConverter PyUnicode_FSConverter -#else // PY3K - -#define PyBytes_Check PyString_Check -#define PyBytes_FromStringAndSize PyString_FromStringAndSize - -#define PyBytes_AsString PyString_AsString -#define PyBytes_AsStringAndSize PyString_AsStringAndSize - -static inline char* PyString_AsUtf8AndSize(PyObject *obj, Py_ssize_t* length) { - char* buffer = NULL; - return (PyString_AsStringAndSize(obj, &buffer, length) < 0) ? (char*)(0) : buffer; -} - -static inline PyObject* PyCreateDummyObject(const char* name) { - PyObject* tmp = Py_InitModule(name, NULL); - Py_INCREF(tmp); - return tmp; -} - -static inline int PyString_FSConverter(PyObject* o, PyObject** p) { - if (o == NULL) { - return 0; - } - - Py_INCREF(o); - *p = o; - return 1; -} - -#endif // PYTHON3 - static inline char* PyBytes_AsStringAndSize2(PyObject *obj, Py_ssize_t* length) { char* buffer = NULL; return ((PyBytes_AsStringAndSize(obj, &buffer, length) < 0) ? (char*)(0) : buffer); diff --git a/src/template.c b/src/template.c index 7d043606..ae0eca34 100644 --- a/src/template.c +++ b/src/template.c @@ -766,10 +766,10 @@ static PyObject* PyXmlSec_TemplateTransformAddC14NInclNamespaces(PyObject* self, goto ON_FAIL; } if (PyList_Check(prefixes) || PyTuple_Check(prefixes)) { - sep = PyString_FromString(" "); + sep = PyUnicode_FromString(" "); prefixes = PyObject_CallMethod(sep, "join", "O", prefixes); Py_DECREF(sep); - } else if (PyString_Check(prefixes)) { + } else if (PyUnicode_Check(prefixes)) { Py_INCREF(prefixes); } else { PyErr_SetString(PyExc_TypeError, "expected instance of str or list of str"); @@ -781,7 +781,7 @@ static PyObject* PyXmlSec_TemplateTransformAddC14NInclNamespaces(PyObject* self, } - c_prefixes = PyString_AsString(prefixes); + c_prefixes = PyUnicode_AsUTF8(prefixes); Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplTransformAddC14NInclNamespaces(node->_c_node, XSTR(c_prefixes)); Py_END_ALLOW_THREADS; @@ -918,7 +918,6 @@ static PyMethodDef PyXmlSec_TemplateMethods[] = { {NULL, NULL} /* sentinel */ }; -#ifdef PY3K static PyModuleDef PyXmlSec_TemplateModule = { PyModuleDef_HEAD_INIT, @@ -931,15 +930,9 @@ static PyModuleDef PyXmlSec_TemplateModule = NULL, /* m_clear */ NULL, /* m_free */ }; -#endif // PY3K int PyXmlSec_TemplateModule_Init(PyObject* package) { -#ifdef PY3K PyObject* template = PyModule_Create(&PyXmlSec_TemplateModule); -#else - PyObject* template = Py_InitModule3(STRINGIFY(MODULE_NAME) ".template", PyXmlSec_TemplateMethods, PYXMLSEC_TEMPLATES_DOC); - Py_XINCREF(template); -#endif if (!template) goto ON_FAIL; PYXMLSEC_DEBUGF("%p", template); diff --git a/src/tree.c b/src/tree.c index 76037d3b..37cae785 100644 --- a/src/tree.c +++ b/src/tree.c @@ -182,7 +182,7 @@ static PyObject* PyXmlSec_TreeAddIds(PyObject* self, PyObject *args, PyObject *k tmp = PyObject_GetItem(ids, key); Py_DECREF(key); if (tmp == NULL) goto ON_FAIL; - list[i] = XSTR(PyString_AsString(tmp)); + list[i] = XSTR(PyUnicode_AsUTF8(tmp)); Py_DECREF(tmp); if (list[i] == NULL) goto ON_FAIL; } @@ -230,7 +230,6 @@ static PyMethodDef PyXmlSec_TreeMethods[] = { {NULL, NULL} /* sentinel */ }; -#ifdef PY3K static PyModuleDef PyXmlSec_TreeModule = { PyModuleDef_HEAD_INIT, @@ -243,16 +242,10 @@ static PyModuleDef PyXmlSec_TreeModule = NULL, /* m_clear */ NULL, /* m_free */ }; -#endif // PY3K int PyXmlSec_TreeModule_Init(PyObject* package) { -#ifdef PY3K PyObject* tree = PyModule_Create(&PyXmlSec_TreeModule); -#else - PyObject* tree = Py_InitModule3(STRINGIFY(MODULE_NAME) ".tree", PyXmlSec_TreeMethods, PYXMLSEC_TREE_DOC); - Py_XINCREF(tree); -#endif if (!tree) goto ON_FAIL; diff --git a/src/utils.c b/src/utils.c index ea6867f3..cdcb182b 100644 --- a/src/utils.c +++ b/src/utils.c @@ -25,14 +25,14 @@ PyObject* PyXmlSec_GetFilePathOrContent(PyObject* file, int* is_content) { return data; } *is_content = 0; - if (!PyString_FSConverter(file, &tmp)) { + if (!PyUnicode_FSConverter(file, &tmp)) { return NULL; } return tmp; } int PyXmlSec_SetStringAttr(PyObject* obj, const char* name, const char* value) { - PyObject* tmp = PyString_FromString(value); + PyObject* tmp = PyUnicode_FromString(value); int r; if (tmp == NULL) { diff --git a/src/xmlsec/__init__.pyi b/src/xmlsec/__init__.pyi index 540553c4..9cfc8cc6 100644 --- a/src/xmlsec/__init__.pyi +++ b/src/xmlsec/__init__.pyi @@ -1,33 +1,38 @@ -import sys -from typing import AnyStr, IO, Iterable, Optional, Type, TypeVar, Union +from collections.abc import Callable, Iterable +from typing import IO, Any, AnyStr, TypeVar, overload +from _typeshed import GenericPath, Self, StrOrBytesPath from lxml.etree import _Element -from xmlsec import constants, template, tree -from xmlsec.constants import __KeyData as KeyData, __Transform as Transform - -if sys.version_info >= (3, 6): - from os import PathLike - from pathlib import PurePath - - _Path = Union[str, bytes, PurePath, PathLike[str], PathLike[bytes]] -elif sys.version_info >= (3, 4): - from pathlib import PurePath - - _Path = Union[str, bytes, PurePath] -else: - _Path = Union[str, bytes] +from xmlsec import constants as constants +from xmlsec import template as template +from xmlsec import tree as tree +from xmlsec.constants import __KeyData as KeyData +from xmlsec.constants import __Transform as Transform _E = TypeVar('_E', bound=_Element) -_K = TypeVar('_K', bound=Key) def enable_debug_trace(enabled: bool = ...) -> None: ... +def get_libxml_version() -> tuple[int, int, int]: ... +def get_libxml_compiled_version() -> tuple[int, int, int]: ... def init() -> None: ... def shutdown() -> None: ... +def cleanup_callbacks() -> None: ... +def register_default_callbacks() -> None: ... +def register_callbacks( + input_match_callback: Callable[[bytes], bool], + input_open_callback: Callable[[bytes], Any], + input_read_callback: Callable[[Any, memoryview], int], + input_close_callback: Callable[[Any], None], +) -> None: ... +@overload +def base64_default_line_size() -> int: ... +@overload +def base64_default_line_size(size: int) -> None: ... class EncryptionContext: - key: Optional[Key] - def __init__(self, manager: Optional[KeysManager] = None) -> None: ... + key: Key | None + def __init__(self, manager: KeysManager | None = ...) -> None: ... def decrypt(self, node: _Element) -> _Element: ... def encrypt_binary(self, template: _E, data: bytes) -> _E: ... def encrypt_uri(self, template: _E, uri: str) -> _E: ... @@ -40,30 +45,32 @@ class InternalError(Error): ... class Key: name: str @classmethod - def from_binary_data(cls: Type[_K], klass: KeyData, data: AnyStr) -> _K: ... + def from_binary_data(cls: type[Self], klass: KeyData, data: AnyStr) -> Self: ... + @classmethod + def from_binary_file(cls: type[Self], klass: KeyData, filename: StrOrBytesPath) -> Self: ... @classmethod - def from_binary_file(cls: Type[_K], klass: KeyData, filename: _Path) -> _K: ... + def from_file(cls: type[Self], file: GenericPath[AnyStr] | IO[AnyStr], format: int, password: str | None = ...) -> Self: ... @classmethod - def from_file(cls: Type[_K], file: Union[_Path, IO[AnyStr]], format: int, password: Optional[str] = ...) -> _K: ... + def from_engine(cls: type[Self], engine_and_key_id: AnyStr) -> Self: ... @classmethod - def from_memory(cls: Type[_K], data: AnyStr, format: int, password: Optional[str] = ...) -> _K: ... + def from_memory(cls: type[Self], data: AnyStr, format: int, password: str | None = ...) -> Self: ... @classmethod - def generate(cls: Type[_K], klass: KeyData, size: int, type: int) -> _K: ... - def load_cert_from_file(self, file: Union[_Path, IO[AnyStr]], format: int) -> None: ... + def generate(cls: type[Self], klass: KeyData, size: int, type: int) -> Self: ... + def load_cert_from_file(self, file: GenericPath[AnyStr] | IO[AnyStr], format: int) -> None: ... def load_cert_from_memory(self, data: AnyStr, format: int) -> None: ... - def __copy__(self: _K) -> _K: ... - def __deepcopy__(self: _K) -> _K: ... + def __copy__(self: Self) -> Self: ... + def __deepcopy__(self: Self) -> Self: ... class KeysManager: def add_key(self, key: Key) -> None: ... - def load_cert(self, filename: _Path, format: int, type: int) -> None: ... + def load_cert(self, filename: StrOrBytesPath, format: int, type: int) -> None: ... def load_cert_from_memory(self, data: AnyStr, format: int, type: int) -> None: ... class SignatureContext: - key: Optional[Key] + key: Key | None def enable_reference_transform(self, transform: Transform) -> None: ... def enable_signature_transform(self, transform: Transform) -> None: ... - def register_id(self, node: _Element, id_attr: str = "ID", id_ns: Optional[str] = None) -> None: ... + def register_id(self, node: _Element, id_attr: str = ..., id_ns: str | None = ...) -> None: ... def set_enabled_key_data(self, keydata_list: Iterable[KeyData]) -> None: ... def sign(self, node: _Element) -> None: ... def sign_binary(self, bytes: bytes, transform: Transform) -> bytes: ... diff --git a/src/xmlsec/constants.pyi b/src/xmlsec/constants.pyi index 71b717a7..a9254ddd 100644 --- a/src/xmlsec/constants.pyi +++ b/src/xmlsec/constants.pyi @@ -1,148 +1,148 @@ import sys -from typing import NamedTuple - -if sys.version_info >= (3, 8): - from typing import Final -else: - from typing_extensions import Final +from typing import Final, NamedTuple class __KeyData(NamedTuple): # __KeyData type href: str name: str +class __KeyDataNoHref(NamedTuple): # __KeyData type + href: None + name: str + class __Transform(NamedTuple): # __Transform type href: str name: str usage: int -DSigNs: Final = 'http://www.w3.org/2000/09/xmldsig#' -EncNs: Final = 'http://www.w3.org/2001/04/xmlenc#' -KeyDataAes: Final = __KeyData('aes', 'http://www.aleksey.com/xmlsec/2002#AESKeyValue') -KeyDataDes: Final = __KeyData('des', 'http://www.aleksey.com/xmlsec/2002#DESKeyValue') -KeyDataDsa: Final = __KeyData('dsa', 'http://www.w3.org/2000/09/xmldsig#DSAKeyValue') -KeyDataEcdsa: Final = __KeyData('ecdsa', 'http://scap.nist.gov/specifications/tmsad/#resource-1.0') -KeyDataEncryptedKey: Final = __KeyData('enc-key', 'http://www.w3.org/2001/04/xmlenc#EncryptedKey') -KeyDataFormatBinary: Final = 1 -KeyDataFormatCertDer: Final = 8 -KeyDataFormatCertPem: Final = 7 -KeyDataFormatDer: Final = 3 -KeyDataFormatPem: Final = 2 -KeyDataFormatPkcs12: Final = 6 -KeyDataFormatPkcs8Der: Final = 5 -KeyDataFormatPkcs8Pem: Final = 4 -KeyDataFormatUnknown: Final = 0 -KeyDataHmac: Final = __KeyData('hmac', 'http://www.aleksey.com/xmlsec/2002#HMACKeyValue') -KeyDataName: Final = __KeyData('key-name', None) -KeyDataRawX509Cert: Final = __KeyData('raw-x509-cert', 'http://www.w3.org/2000/09/xmldsig#rawX509Certificate') -KeyDataRetrievalMethod: Final = __KeyData('retrieval-method', None) -KeyDataRsa: Final = __KeyData('rsa', 'http://www.w3.org/2000/09/xmldsig#RSAKeyValue') -KeyDataTypeAny: Final = 65535 -KeyDataTypeNone: Final = 0 -KeyDataTypePermanent: Final = 16 -KeyDataTypePrivate: Final = 2 -KeyDataTypePublic: Final = 1 -KeyDataTypeSession: Final = 8 -KeyDataTypeSymmetric: Final = 4 -KeyDataTypeTrusted: Final = 256 -KeyDataTypeUnknown: Final = 0 -KeyDataValue: Final = __KeyData('key-value', None) -KeyDataX509: Final = __KeyData('x509', 'http://www.w3.org/2000/09/xmldsig#X509Data') -NodeCanonicalizationMethod: Final = 'CanonicalizationMethod' -NodeCipherData: Final = 'CipherData' -NodeCipherReference: Final = 'CipherReference' -NodeCipherValue: Final = 'CipherValue' -NodeDataReference: Final = 'DataReference' -NodeDigestMethod: Final = 'DigestMethod' -NodeDigestValue: Final = 'DigestValue' -NodeEncryptedData: Final = 'EncryptedData' -NodeEncryptedKey: Final = 'EncryptedKey' -NodeEncryptionMethod: Final = 'EncryptionMethod' -NodeEncryptionProperties: Final = 'EncryptionProperties' -NodeEncryptionProperty: Final = 'EncryptionProperty' -NodeKeyInfo: Final = 'KeyInfo' -NodeKeyName: Final = 'KeyName' -NodeKeyReference: Final = 'KeyReference' -NodeKeyValue: Final = 'KeyValue' -NodeManifest: Final = 'Manifest' -NodeObject: Final = 'Object' -NodeReference: Final = 'Reference' -NodeReferenceList: Final = 'ReferenceList' -NodeSignature: Final = 'Signature' -NodeSignatureMethod: Final = 'SignatureMethod' -NodeSignatureProperties: Final = 'SignatureProperties' -NodeSignatureValue: Final = 'SignatureValue' -NodeSignedInfo: Final = 'SignedInfo' -NodeX509Data: Final = 'X509Data' -Ns: Final = 'http://www.aleksey.com/xmlsec/2002' -NsExcC14N: Final = 'http://www.w3.org/2001/10/xml-exc-c14n#' -NsExcC14NWithComments: Final = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments' -Soap11Ns: Final = 'http://schemas.xmlsoap.org/soap/envelope/' -Soap12Ns: Final = 'http://www.w3.org/2002/06/soap-envelope' -TransformAes128Cbc: Final = __Transform('aes128-cbc', 'http://www.w3.org/2001/04/xmlenc#aes128-cbc', 16) -TransformAes192Cbc: Final = __Transform('aes192-cbc', 'http://www.w3.org/2001/04/xmlenc#aes192-cbc', 16) -TransformAes256Cbc: Final = __Transform('aes256-cbc', 'http://www.w3.org/2001/04/xmlenc#aes256-cbc', 16) -TransformDes3Cbc: Final = __Transform('tripledes-cbc', 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc', 16) -TransformDsaSha1: Final = __Transform('dsa-sha1', 'http://www.w3.org/2000/09/xmldsig#dsa-sha1', 8) -TransformEcdsaSha1: Final = __Transform('ecdsa-sha1', 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1', 8) -TransformEcdsaSha224: Final = __Transform('ecdsa-sha224', 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224', 8) -TransformEcdsaSha256: Final = __Transform('ecdsa-sha256', 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256', 8) -TransformEcdsaSha384: Final = __Transform('ecdsa-sha384', 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384', 8) -TransformEcdsaSha512: Final = __Transform('ecdsa-sha512', 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512', 8) -TransformEnveloped: Final = __Transform('enveloped-signature', 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 1) -TransformExclC14N: Final = __Transform('exc-c14n', 'http://www.w3.org/2001/10/xml-exc-c14n#', 3) -TransformExclC14NWithComments: Final = __Transform( - 'exc-c14n-with-comments', 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments', 3 -) -TransformHmacMd5: Final = __Transform('hmac-md5', 'http://www.w3.org/2001/04/xmldsig-more#hmac-md5', 8) -TransformHmacRipemd160: Final = __Transform('hmac-ripemd160', 'http://www.w3.org/2001/04/xmldsig-more#hmac-ripemd160', 8) -TransformHmacSha1: Final = __Transform('hmac-sha1', 'http://www.w3.org/2000/09/xmldsig#hmac-sha1', 8) -TransformHmacSha224: Final = __Transform('hmac-sha224', 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha224', 8) -TransformHmacSha256: Final = __Transform('hmac-sha256', 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha256', 8) -TransformHmacSha384: Final = __Transform('hmac-sha384', 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha384', 8) -TransformHmacSha512: Final = __Transform('hmac-sha512', 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha512', 8) -TransformInclC14N: Final = __Transform('c14n', 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315', 3) -TransformInclC14N11: Final = __Transform('c14n11', 'http://www.w3.org/2006/12/xml-c14n11', 3) -TransformInclC14N11WithComments: Final = __Transform( - 'c14n11-with-comments', 'http://www.w3.org/2006/12/xml-c14n11#WithComments', 3 -) -TransformInclC14NWithComments: Final = __Transform( - 'c14n-with-comments', 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments', 3 -) -TransformKWAes128: Final = __Transform('kw-aes128', 'http://www.w3.org/2001/04/xmlenc#kw-aes128', 16) -TransformKWAes192: Final = __Transform('kw-aes192', 'http://www.w3.org/2001/04/xmlenc#kw-aes192', 16) -TransformKWAes256: Final = __Transform('kw-aes256', 'http://www.w3.org/2001/04/xmlenc#kw-aes256', 16) -TransformKWDes3: Final = __Transform('kw-tripledes', 'http://www.w3.org/2001/04/xmlenc#kw-tripledes', 16) -TransformMd5: Final = __Transform('md5', 'http://www.w3.org/2001/04/xmldsig-more#md5', 4) -TransformRemoveXmlTagsC14N: Final = __Transform('remove-xml-tags-transform', None, 3) -TransformRipemd160: Final = __Transform('ripemd160', 'http://www.w3.org/2001/04/xmlenc#ripemd160', 4) -TransformRsaMd5: Final = __Transform('rsa-md5', 'http://www.w3.org/2001/04/xmldsig-more#rsa-md5', 8) -TransformRsaOaep: Final = __Transform('rsa-oaep-mgf1p', 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p', 16) -TransformRsaPkcs1: Final = __Transform('rsa-1_5', 'http://www.w3.org/2001/04/xmlenc#rsa-1_5', 16) -TransformRsaRipemd160: Final = __Transform('rsa-ripemd160', 'http://www.w3.org/2001/04/xmldsig-more#rsa-ripemd160', 8) -TransformRsaSha1: Final = __Transform('rsa-sha1', 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 8) -TransformRsaSha224: Final = __Transform('rsa-sha224', 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha224', 8) -TransformRsaSha256: Final = __Transform('rsa-sha256', 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', 8) -TransformRsaSha384: Final = __Transform('rsa-sha384', 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384', 8) -TransformRsaSha512: Final = __Transform('rsa-sha512', 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512', 8) -TransformSha1: Final = __Transform('sha1', 'http://www.w3.org/2000/09/xmldsig#sha1', 4) -TransformSha224: Final = __Transform('sha224', 'http://www.w3.org/2001/04/xmldsig-more#sha224', 4) -TransformSha256: Final = __Transform('sha256', 'http://www.w3.org/2001/04/xmlenc#sha256', 4) -TransformSha384: Final = __Transform('sha384', 'http://www.w3.org/2001/04/xmldsig-more#sha384', 4) -TransformSha512: Final = __Transform('sha512', 'http://www.w3.org/2001/04/xmlenc#sha512', 4) -TransformUsageAny: Final = 65535 -TransformUsageC14NMethod: Final = 2 -TransformUsageDSigTransform: Final = 1 -TransformUsageDigestMethod: Final = 4 -TransformUsageEncryptionMethod: Final = 16 -TransformUsageSignatureMethod: Final = 8 -TransformUsageUnknown: Final = 0 -TransformVisa3DHack: Final = __Transform('Visa3DHackTransform', None, 1) -TransformXPath: Final = __Transform('xpath', 'http://www.w3.org/TR/1999/REC-xpath-19991116', 1) -TransformXPath2: Final = __Transform('xpath2', 'http://www.w3.org/2002/06/xmldsig-filter2', 1) -TransformXPointer: Final = __Transform('xpointer', 'http://www.w3.org/2001/04/xmldsig-more/xptr', 1) -TransformXslt: Final = __Transform('xslt', 'http://www.w3.org/TR/1999/REC-xslt-19991116', 1) -TypeEncContent: Final = 'http://www.w3.org/2001/04/xmlenc#Content' -TypeEncElement: Final = 'http://www.w3.org/2001/04/xmlenc#Element' -XPath2Ns: Final = 'http://www.w3.org/2002/06/xmldsig-filter2' -XPathNs: Final = 'http://www.w3.org/TR/1999/REC-xpath-19991116' -XPointerNs: Final = 'http://www.w3.org/2001/04/xmldsig-more/xptr' +class __TransformNoHref(NamedTuple): # __Transform type + href: None + name: str + usage: int + +DSigNs: Final[str] +EncNs: Final[str] +KeyDataAes: Final[__KeyData] +KeyDataDes: Final[__KeyData] +KeyDataDsa: Final[__KeyData] +KeyDataEc: Final[__KeyData] +KeyDataEcdsa: Final[__KeyData] +KeyDataEncryptedKey: Final[__KeyData] +KeyDataFormatBinary: Final[int] +KeyDataFormatCertDer: Final[int] +KeyDataFormatCertPem: Final[int] +KeyDataFormatDer: Final[int] +KeyDataFormatPem: Final[int] +KeyDataFormatPkcs12: Final[int] +KeyDataFormatPkcs8Der: Final[int] +KeyDataFormatPkcs8Pem: Final[int] +KeyDataFormatUnknown: Final[int] +KeyDataHmac: Final[__KeyData] +KeyDataName: Final[__KeyDataNoHref] +KeyDataRawX509Cert: Final[__KeyData] +KeyDataRetrievalMethod: Final[__KeyDataNoHref] +KeyDataRsa: Final[__KeyData] +KeyDataTypeAny: Final[int] +KeyDataTypeNone: Final[int] +KeyDataTypePermanent: Final[int] +KeyDataTypePrivate: Final[int] +KeyDataTypePublic: Final[int] +KeyDataTypeSession: Final[int] +KeyDataTypeSymmetric: Final[int] +KeyDataTypeTrusted: Final[int] +KeyDataTypeUnknown: Final[int] +KeyDataValue: Final[__KeyDataNoHref] +KeyDataX509: Final[__KeyData] +NodeCanonicalizationMethod: Final[str] +NodeCipherData: Final[str] +NodeCipherReference: Final[str] +NodeCipherValue: Final[str] +NodeDataReference: Final[str] +NodeDigestMethod: Final[str] +NodeDigestValue: Final[str] +NodeEncryptedData: Final[str] +NodeEncryptedKey: Final[str] +NodeEncryptionMethod: Final[str] +NodeEncryptionProperties: Final[str] +NodeEncryptionProperty: Final[str] +NodeKeyInfo: Final[str] +NodeKeyName: Final[str] +NodeKeyReference: Final[str] +NodeKeyValue: Final[str] +NodeManifest: Final[str] +NodeObject: Final[str] +NodeReference: Final[str] +NodeReferenceList: Final[str] +NodeSignature: Final[str] +NodeSignatureMethod: Final[str] +NodeSignatureProperties: Final[str] +NodeSignatureValue: Final[str] +NodeSignedInfo: Final[str] +NodeX509Data: Final[str] +Ns: Final[str] +NsExcC14N: Final[str] +NsExcC14NWithComments: Final[str] +TransformAes128Cbc: Final[__Transform] +TransformAes128Gcm: Final[__Transform] +TransformAes192Cbc: Final[__Transform] +TransformAes192Gcm: Final[__Transform] +TransformAes256Cbc: Final[__Transform] +TransformAes256Gcm: Final[__Transform] +TransformDes3Cbc: Final[__Transform] +TransformDsaSha1: Final[__Transform] +TransformEcdsaSha1: Final[__Transform] +TransformEcdsaSha224: Final[__Transform] +TransformEcdsaSha256: Final[__Transform] +TransformEcdsaSha384: Final[__Transform] +TransformEcdsaSha512: Final[__Transform] +TransformEnveloped: Final[__Transform] +TransformExclC14N: Final[__Transform] +TransformExclC14NWithComments: Final[__Transform] +TransformHmacMd5: Final[__Transform] +TransformHmacRipemd160: Final[__Transform] +TransformHmacSha1: Final[__Transform] +TransformHmacSha224: Final[__Transform] +TransformHmacSha256: Final[__Transform] +TransformHmacSha384: Final[__Transform] +TransformHmacSha512: Final[__Transform] +TransformInclC14N: Final[__Transform] +TransformInclC14N11: Final[__Transform] +TransformInclC14N11WithComments: Final[__Transform] +TransformInclC14NWithComments: Final[__Transform] +TransformKWAes128: Final[__Transform] +TransformKWAes192: Final[__Transform] +TransformKWAes256: Final[__Transform] +TransformKWDes3: Final[__Transform] +TransformMd5: Final[__Transform] +TransformRemoveXmlTagsC14N: Final[__TransformNoHref] +TransformRipemd160: Final[__Transform] +TransformRsaMd5: Final[__Transform] +TransformRsaOaep: Final[__Transform] +TransformRsaPkcs1: Final[__Transform] +TransformRsaRipemd160: Final[__Transform] +TransformRsaSha1: Final[__Transform] +TransformRsaSha224: Final[__Transform] +TransformRsaSha256: Final[__Transform] +TransformRsaSha384: Final[__Transform] +TransformRsaSha512: Final[__Transform] +TransformSha1: Final[__Transform] +TransformSha224: Final[__Transform] +TransformSha256: Final[__Transform] +TransformSha384: Final[__Transform] +TransformSha512: Final[__Transform] +TransformUsageAny: Final[int] +TransformUsageC14NMethod: Final[int] +TransformUsageDSigTransform: Final[int] +TransformUsageDigestMethod: Final[int] +TransformUsageEncryptionMethod: Final[int] +TransformUsageSignatureMethod: Final[int] +TransformUsageUnknown: Final[int] +TransformVisa3DHack: Final[__TransformNoHref] +TransformXPath: Final[__Transform] +TransformXPath2: Final[__Transform] +TransformXPointer: Final[__Transform] +TransformXslt: Final[__Transform] +TypeEncContent: Final[str] +TypeEncElement: Final[str] +XPath2Ns: Final[str] +XPathNs: Final[str] +XPointerNs: Final[str] diff --git a/src/xmlsec/template.pyi b/src/xmlsec/template.pyi index 162fe25d..d1755fa2 100644 --- a/src/xmlsec/template.pyi +++ b/src/xmlsec/template.pyi @@ -1,37 +1,38 @@ -from typing import Any, Optional, Sequence, Union +from collections.abc import Sequence +from typing import Any from lxml.etree import _Element from xmlsec.constants import __Transform as Transform def add_encrypted_key( - node: _Element, method: Transform, id: Optional[str] = None, type: Optional[str] = None, recipient: Optional[str] = None + node: _Element, method: Transform, id: str | None = ..., type: str | None = ..., recipient: str | None = ... ) -> _Element: ... -def add_key_name(node: _Element, name: Optional[str] = ...) -> _Element: ... +def add_key_name(node: _Element, name: str | None = ...) -> _Element: ... def add_key_value(node: _Element) -> _Element: ... def add_reference( - node: _Element, digest_method: Transform, id: Optional[str] = ..., uri: Optional[str] = ..., type: Optional[str] = ... + node: _Element, digest_method: Transform, id: str | None = ..., uri: str | None = ..., type: str | None = ... ) -> _Element: ... def add_transform(node: _Element, transform: Transform) -> Any: ... def add_x509_data(node: _Element) -> _Element: ... -def create(node: _Element, c14n_method: Transform, sign_method: Transform) -> _Element: ... +def create(node: _Element, c14n_method: Transform, sign_method: Transform, id: str | None = ..., ns: str | None = ...) -> _Element: ... def encrypted_data_create( node: _Element, method: Transform, - id: Optional[str] = ..., - type: Optional[str] = ..., - mime_type: Optional[str] = ..., - encoding: Optional[str] = ..., - ns: Optional[str] = ..., + id: str | None = ..., + type: str | None = ..., + mime_type: str | None = ..., + encoding: str | None = ..., + ns: str | None = ..., ) -> _Element: ... def encrypted_data_ensure_cipher_value(node: _Element) -> _Element: ... -def encrypted_data_ensure_key_info(node: _Element, id: Optional[str] = ..., ns: Optional[str] = ...) -> _Element: ... -def ensure_key_info(node: _Element, id: Optional[str] = ...) -> _Element: ... -def transform_add_c14n_inclusive_namespaces(node: _Element, prefixes: Union[str, Sequence[str]]) -> None: ... +def encrypted_data_ensure_key_info(node: _Element, id: str | None = ..., ns: str | None = ...) -> _Element: ... +def ensure_key_info(node: _Element, id: str | None = ...) -> _Element: ... +def transform_add_c14n_inclusive_namespaces(node: _Element, prefixes: str | Sequence[str]) -> None: ... def x509_data_add_certificate(node: _Element) -> _Element: ... def x509_data_add_crl(node: _Element) -> _Element: ... def x509_data_add_issuer_serial(node: _Element) -> _Element: ... def x509_data_add_ski(node: _Element) -> _Element: ... def x509_data_add_subject_name(node: _Element) -> _Element: ... -def x509_issuer_serial_add_issuer_name(node: _Element, name: Optional[str] = ...) -> _Element: ... -def x509_issuer_serial_add_serial_number(node: _Element, serial: Optional[str] = ...) -> _Element: ... +def x509_issuer_serial_add_issuer_name(node: _Element, name: str | None = ...) -> _Element: ... +def x509_issuer_serial_add_serial_number(node: _Element, serial: str | None = ...) -> _Element: ... diff --git a/src/xmlsec/tree.pyi b/src/xmlsec/tree.pyi index 6447fd08..9f96e447 100644 --- a/src/xmlsec/tree.pyi +++ b/src/xmlsec/tree.pyi @@ -1,17 +1,18 @@ -from typing import Optional, overload, Sequence +from collections.abc import Sequence +from typing import overload from lxml.etree import _Element def add_ids(node: _Element, ids: Sequence[str]) -> None: ... @overload -def find_child(parent: _Element, name: str) -> Optional[_Element]: ... +def find_child(parent: _Element, name: str) -> _Element | None: ... @overload -def find_child(parent: _Element, name: str, namespace: str = ...) -> Optional[_Element]: ... +def find_child(parent: _Element, name: str, namespace: str = ...) -> _Element | None: ... @overload -def find_node(node: _Element, name: str) -> Optional[_Element]: ... +def find_node(node: _Element, name: str) -> _Element | None: ... @overload -def find_node(node: _Element, name: str, namespace: str = ...) -> Optional[_Element]: ... +def find_node(node: _Element, name: str, namespace: str = ...) -> _Element | None: ... @overload -def find_parent(node: _Element, name: str) -> Optional[_Element]: ... +def find_parent(node: _Element, name: str) -> _Element | None: ... @overload -def find_parent(node: _Element, name: str, namespace: str = ...) -> Optional[_Element]: ... +def find_parent(node: _Element, name: str, namespace: str = ...) -> _Element | None: ... diff --git a/tests/base.py b/tests/base.py index b05de1de..1d21c89b 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,14 +1,13 @@ import gc import os import sys +import unittest from lxml import etree -import xmlsec - -import unittest +import xmlsec -etype = type(etree.Element("test")) +etype = type(etree.Element('test')) ns = {'dsig': xmlsec.constants.DSigNs, 'enc': xmlsec.constants.EncNs} @@ -16,40 +15,26 @@ try: import resource - def get_memory_usage(): - return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss -except ImportError: - resource = None - - def get_memory_usage(): - return 0 - - -def get_iterations(): - if sys.platform in ('win32',): - return 0 - - try: - return int(os.getenv("PYXMLSEC_TEST_ITERATIONS", "10")) - except ValueError: - return 0 + test_iterations = int(os.environ.get('PYXMLSEC_TEST_ITERATIONS', '10')) +except (ImportError, ValueError): + test_iterations = 0 class TestMemoryLeaks(unittest.TestCase): maxDiff = None - iterations = get_iterations() + iterations = test_iterations - data_dir = os.path.join(os.path.dirname(__file__), "data") + data_dir = os.path.join(os.path.dirname(__file__), 'data') def setUp(self): gc.disable() - self.addTypeEqualityFunc(etype, "assertXmlEqual") + self.addTypeEqualityFunc(etype, 'assertXmlEqual') xmlsec.enable_debug_trace(1) def run(self, result=None): # run first time - super(TestMemoryLeaks, self).run(result=result) + super().run(result=result) if self.iterations == 0: return @@ -57,8 +42,8 @@ def run(self, result=None): o_count = gc.get_count()[0] m_hits = 0 o_hits = 0 - for i in range(self.iterations): - super(TestMemoryLeaks, self).run(result=result) + for _ in range(self.iterations): + super().run(result=result) m_usage_n = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss if m_usage_n > m_usage: m_usage = m_usage_n @@ -73,63 +58,60 @@ def run(self, result=None): if m_hits > int(self.iterations * 0.8): result.buffer = False try: - raise AssertionError("memory leak detected") + raise AssertionError('memory leak detected') except AssertionError: result.addError(self, sys.exc_info()) if o_hits > int(self.iterations * 0.8): result.buffer = False try: - raise AssertionError("unreferenced objects detected") + raise AssertionError('unreferenced objects detected') except AssertionError: result.addError(self, sys.exc_info()) def path(self, name): - """returns full path for resource""" + """Return full path for resource.""" return os.path.join(self.data_dir, name) def load(self, name): - """loads resource by name""" - with open(self.path(name), "rb") as stream: + """Load resource by name.""" + with open(self.path(name), 'rb') as stream: return stream.read() def load_xml(self, name, xpath=None): - """returns xml.etree""" - root = etree.parse(self.path(name)).getroot() - if xpath is None: - return root - return root.find(xpath) + """Return xml.etree.""" + with open(self.path(name)) as f: + root = etree.parse(f).getroot() + if xpath is None: + return root + return root.find(xpath) def dump(self, root): print(etree.tostring(root)) def assertXmlEqual(self, first, second, msg=None): - """Checks equality of etree.roots""" + """Check equality of etree.roots.""" msg = msg or '' if first.tag != second.tag: - self.fail('Tags do not match: %s and %s. %s' % (first.tag, second.tag, msg)) + self.fail(f'Tags do not match: {first.tag} and {second.tag}. {msg}') for name, value in first.attrib.items(): if second.attrib.get(name) != value: - self.fail( - 'Attributes do not match: %s=%r, %s=%r. %s' % (name, value, name, second.attrib.get(name), msg) - ) - for name in second.attrib.keys(): + self.fail(f'Attributes do not match: {name}={value!r}, {name}={second.attrib.get(name)!r}. {msg}') + for name in second.attrib: if name not in first.attrib: - self.fail('x2 has an attribute x1 is missing: %s. %s' % (name, msg)) - if not xml_text_compare(first.text, second.text): - self.fail('text: %r != %r. %s' % (first.text, second.text, msg)) - if not xml_text_compare(first.tail, second.tail): - self.fail('tail: %r != %r. %s' % (first.tail, second.tail, msg)) + self.fail(f'x2 has an attribute x1 is missing: {name}. {msg}') + if not _xml_text_compare(first.text, second.text): + self.fail(f'text: {first.text!r} != {second.text!r}. {msg}') + if not _xml_text_compare(first.tail, second.tail): + self.fail(f'tail: {first.tail!r} != {second.tail!r}. {msg}') cl1 = sorted(first.getchildren(), key=lambda x: x.tag) cl2 = sorted(second.getchildren(), key=lambda x: x.tag) if len(cl1) != len(cl2): - self.fail('children length differs, %i != %i. %s' % (len(cl1), len(cl2), msg)) - i = 0 + self.fail(f'children length differs, {len(cl1)} != {len(cl2)}. {msg}') for c1, c2 in zip(cl1, cl2): - i += 1 self.assertXmlEqual(c1, c2) -def xml_text_compare(t1, t2): +def _xml_text_compare(t1, t2): if not t1 and not t2: return True if t1 == '*' or t2 == '*': diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..a65235d5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +def pytest_collection_modifyitems(items): + """Put the module init test first. + + This way, we implicitly check whether any subsequent test fails because of module reinitialization. + """ + + def module_init_tests_first(item): + return int('test_xmlsec.py::TestModule::test_reinitialize_module' not in item.nodeid) + + items.sort(key=module_init_tests_first) diff --git a/tests/data/deskey.bin b/tests/data/deskey.bin index 019924a7..73245c3c 100644 --- a/tests/data/deskey.bin +++ b/tests/data/deskey.bin @@ -1 +1 @@ -012345670123456701234567 \ No newline at end of file +012345670123456701234567 diff --git a/tests/data/doc.xml b/tests/data/doc.xml index fd474859..39f8d761 100644 --- a/tests/data/doc.xml +++ b/tests/data/doc.xml @@ -4,4 +4,4 @@ XML Security Library example: Original XML doc file for enc example. --> Hello, World! - \ No newline at end of file + diff --git a/tests/data/enc-bad-in.xml b/tests/data/enc-bad-in.xml deleted file mode 100644 index 460738fc..00000000 --- a/tests/data/enc-bad-in.xml +++ /dev/null @@ -1,208 +0,0 @@ - - - - - - - - - MyNextCar - CreditApplication - MYNEXTCAR - VW - 409D03 - MyNextCar - - 2018-11-20T09:37:45Z - 7f0842cc-8d47-4955-be31-c61d07ee490b - - VW - - - - - - -
- VCI_MNA_0000070250 - - - Car Chantilly -
- 14839 Stonecroft Center Ct - Chantilly - VA - US - 20151 -
- - - MyNextCar - MNA - - 7039562100 - - CAR -
- N -
- - - 2017 - Q7 - CAR - New - 0 - Prestige - - 64300.0 - MSRP - - - 64300.0 - Selling Price - - - - - 113456789 - NationalId - - - John - Q - Public - -
- 999 Washington Ave - Apt #332 - Front Royal - VA - US - 22630 - 01 - 10 - Own -
-
- 21 E 9th Ave - Boulder - CO - US - 80301-7577 - 07 - 11 - Own -
- - 3032852402 - 3032852405 - 7203554444 - JohnQPublic@anydomain.org - - - 1967-07-31 - - 0 - - UPS -
- 1775 Wiehle Ave. - Reston - VA - US - 20190 -
- 9500.0 - Driver - 01 - 05 - Current -
- - FedEx - 4000.00 - Driver - 04 - 09 - Previous - - 1252.52 - - 1500.00 - - - 1 - Consents to Credit Check - -
- - - 123435325 - NationalId - - - Lisa - C - Public - -
- 999 Lewis Street - Front Royal - VA - US - 22630 - 5 - 0 - Own -
- - 5401110000 - 5401110073 - public@test.com - - - 1963-04-20 - - - Christendom College -
- 999 Christendom Dr - Front Royal - VA - US - 22630 -
- 6200.00 - Professor - 5 - 0 - Current -
- 1252.52 - - 1 - Consents to Credit Check - -
- - R - 0.00 - 66 - 5000.00 - INDIVCOAPP - 2000.00 - MyNextCar - - 1978 - Bonneville - Pontiac - Coupe - - -
-
-
-
-
-
diff --git a/tests/data/enc1-in.xml b/tests/data/enc1-in.xml index fd474859..39f8d761 100644 --- a/tests/data/enc1-in.xml +++ b/tests/data/enc1-in.xml @@ -4,4 +4,4 @@ XML Security Library example: Original XML doc file for enc example. --> Hello, World! - \ No newline at end of file + diff --git a/tests/data/enc1-out.xml b/tests/data/enc1-out.xml index 9499453b..ab0b1a6c 100644 --- a/tests/data/enc1-out.xml +++ b/tests/data/enc1-out.xml @@ -19,4 +19,4 @@ DY/U86tTpTn95NwHD10SLyrL6rpXdbEuoIQHhWLwV9uQxnJA/Pn1KZ+xXK/fePfP 2pb5Mxd0f+AW56Cs3MfQ9HJkUVeliSi1hVCNCVHTKeMyC2VL6lPhQ9+L01aSeTSY - \ No newline at end of file + diff --git a/tests/data/enc2-out.xml b/tests/data/enc2-out.xml index 6556248e..4b3b5c34 100644 --- a/tests/data/enc2-out.xml +++ b/tests/data/enc2-out.xml @@ -19,4 +19,4 @@ CTBwsOXCAEJYXPkTrnB3qQ== 4m5BRKEswOe8JISY7NrPGLBYv7Ay5pBV+nG6it51gz0= - \ No newline at end of file + diff --git a/tests/data/rsacert.pem b/tests/data/rsacert.pem index 02489a43..e8a68228 100644 --- a/tests/data/rsacert.pem +++ b/tests/data/rsacert.pem @@ -32,13 +32,13 @@ Certificate: 65:c3 Exponent: 65537 (0x10001) X509v3 extensions: - X509v3 Basic Constraints: + X509v3 Basic Constraints: CA:FALSE - Netscape Comment: + Netscape Comment: OpenSSL Generated Certificate - X509v3 Subject Key Identifier: + X509v3 Subject Key Identifier: 24:84:2C:F2:D4:59:20:62:8B:2E:5C:86:90:A3:AA:30:BA:27:1A:9C - X509v3 Authority Key Identifier: + X509v3 Authority Key Identifier: keyid:B4:B9:EF:9A:E6:97:0E:68:65:1E:98:CE:FA:55:0D:89:06:DB:4C:7C DirName:/C=US/ST=California/L=Sunnyvale/O=XML Security Library (http://www.aleksey.com/xmlsec)/OU=Root Certificate/CN=Aleksey Sanin/emailAddress=xmlsec@aleksey.com serial:00 diff --git a/tests/data/sign1-in.xml b/tests/data/sign1-in.xml index ac71a949..0a0cd442 100644 --- a/tests/data/sign1-in.xml +++ b/tests/data/sign1-in.xml @@ -1,6 +1,6 @@ - @@ -24,4 +24,3 @@ XML Security Library example: Simple signature template file for sign1 example. - diff --git a/tests/data/sign1-out.xml b/tests/data/sign1-out.xml index 04d8fed0..f46ac1f4 100644 --- a/tests/data/sign1-out.xml +++ b/tests/data/sign1-out.xml @@ -1,6 +1,6 @@ - diff --git a/tests/data/sign2-in.xml b/tests/data/sign2-in.xml index 5d9fb352..2f2592f2 100644 --- a/tests/data/sign2-in.xml +++ b/tests/data/sign2-in.xml @@ -1,6 +1,6 @@ - diff --git a/tests/data/sign2-out.xml b/tests/data/sign2-out.xml index b37cad94..b5782d6c 100644 --- a/tests/data/sign2-out.xml +++ b/tests/data/sign2-out.xml @@ -1,6 +1,6 @@ - diff --git a/tests/data/sign3-in.xml b/tests/data/sign3-in.xml index f75da16a..96260b8f 100644 --- a/tests/data/sign3-in.xml +++ b/tests/data/sign3-in.xml @@ -1,6 +1,6 @@ - diff --git a/tests/data/sign3-out.xml b/tests/data/sign3-out.xml index 847e1af2..b7bf15c3 100644 --- a/tests/data/sign3-out.xml +++ b/tests/data/sign3-out.xml @@ -1,6 +1,6 @@ - diff --git a/tests/data/sign4-in.xml b/tests/data/sign4-in.xml index cc00479b..d49fc3ae 100644 --- a/tests/data/sign4-in.xml +++ b/tests/data/sign4-in.xml @@ -1,6 +1,6 @@ - diff --git a/tests/data/sign4-out.xml b/tests/data/sign4-out.xml index a6fecb44..bdd1014e 100644 --- a/tests/data/sign4-out.xml +++ b/tests/data/sign4-out.xml @@ -52,4 +52,4 @@ ss0uc1NxfahMaBoyG15IL4+beqO182fosaKJTrJNG3mc//ANGU9OsQM9mfBEt4oL NJ2D - \ No newline at end of file + diff --git a/tests/data/sign5-out-xmlsec_1_2_36_to_37.xml b/tests/data/sign5-out-xmlsec_1_2_36_to_37.xml new file mode 100644 index 00000000..f359b138 --- /dev/null +++ b/tests/data/sign5-out-xmlsec_1_2_36_to_37.xml @@ -0,0 +1,67 @@ + + + + + Hello, World! + + + + + + + + + + +HjY8ilZAIEM2tBbPn5mYO1ieIX4= + + +SIaj/6KY3C1SmDXU2++Gm31U1xTadFp04WhBgfsJFbxrL+q7GKSKN9kfQ+UpN9+i +D5fWmuavXEHe4Gw6RMaMEkq2URQo7F68+d5J/ajq8/l4n+xE6/reGScVwT6L4dEP +XXVJcAi2ZnQ3O7GTNvNGCPibL9mUcyCWBFZ92Uemtc/vJFCQ7ZyKMdMfACgxOwyN +T/9971oog241/2doudhonc0I/3mgPYWkZdX6yvr62mEjnG+oUZkhWYJ4ewZJ4hM4 +JjbFqZO+OEzDRSbw3DkmuBA/mtlx+3t13SESfEub5hqoMdVmtth/eTb64dsPdl9r +3k1ACVX9f8aHfQQdJOmLFQ== + + + + + + +Test Issuer +1 + +MIIE3zCCBEigAwIBAgIBBTANBgkqhkiG9w0BAQQFADCByzELMAkGA1UEBhMCVVMx +EzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVN1bm55dmFsZTE9MDsGA1UE +ChM0WE1MIFNlY3VyaXR5IExpYnJhcnkgKGh0dHA6Ly93d3cuYWxla3NleS5jb20v +eG1sc2VjKTEZMBcGA1UECxMQUm9vdCBDZXJ0aWZpY2F0ZTEWMBQGA1UEAxMNQWxl +a3NleSBTYW5pbjEhMB8GCSqGSIb3DQEJARYSeG1sc2VjQGFsZWtzZXkuY29tMB4X +DTAzMDMzMTA0MDIyMloXDTEzMDMyODA0MDIyMlowgb8xCzAJBgNVBAYTAlVTMRMw +EQYDVQQIEwpDYWxpZm9ybmlhMT0wOwYDVQQKEzRYTUwgU2VjdXJpdHkgTGlicmFy +eSAoaHR0cDovL3d3dy5hbGVrc2V5LmNvbS94bWxzZWMpMSEwHwYDVQQLExhFeGFt +cGxlcyBSU0EgQ2VydGlmaWNhdGUxFjAUBgNVBAMTDUFsZWtzZXkgU2FuaW4xITAf +BgkqhkiG9w0BCQEWEnhtbHNlY0BhbGVrc2V5LmNvbTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAJe4/rQ/gzV4FokE7CthjL/EXwCBSkXm2c3p4jyXO0Wt +quaNC3dxBwFPfPl94hmq3ZFZ9PHPPbp4RpYRnLZbRjlzVSOq954AXOXpSew7nD+E +mTqQrd9+ZIbGJnLOMQh5fhMVuOW/1lYCjWAhTCcYZPv7VXD2M70vVXDVXn6ZrqTg +qkVHE6gw1aCKncwg7OSOUclUxX8+Zi10v6N6+PPslFc5tKwAdWJhVLTQ4FKG+F53 +7FBDnNK6p4xiWryy/vPMYn4jYGvHUUk3eH4lFTCr+rSuJY8i/KNIf/IKim7g/o3w +Ae3GM8xrof2mgO8GjK/2QDqOQhQgYRIf4/wFsQXVZcMCAwEAAaOCAVcwggFTMAkG +A1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRp +ZmljYXRlMB0GA1UdDgQWBBQkhCzy1FkgYosuXIaQo6owuicanDCB+AYDVR0jBIHw +MIHtgBS0ue+a5pcOaGUemM76VQ2JBttMfKGB0aSBzjCByzELMAkGA1UEBhMCVVMx +EzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVN1bm55dmFsZTE9MDsGA1UE +ChM0WE1MIFNlY3VyaXR5IExpYnJhcnkgKGh0dHA6Ly93d3cuYWxla3NleS5jb20v +eG1sc2VjKTEZMBcGA1UECxMQUm9vdCBDZXJ0aWZpY2F0ZTEWMBQGA1UEAxMNQWxl +a3NleSBTYW5pbjEhMB8GCSqGSIb3DQEJARYSeG1sc2VjQGFsZWtzZXkuY29tggEA +MA0GCSqGSIb3DQEBBAUAA4GBALU/mzIxSv8vhDuomxFcplzwdlLZbvSQrfoNkMGY +1UoS3YJrN+jZLWKSyWE3mIaPpElqXiXQGGkwD5iPQ1iJMbI7BeLvx6ZxX/f+c8Wn +ss0uc1NxfahMaBoyG15IL4+beqO182fosaKJTrJNG3mc//ANGU9OsQM9mfBEt4oL +NJ2D + + + + + diff --git a/tests/softhsm_setup.py b/tests/softhsm_setup.py new file mode 100644 index 00000000..d55c16fd --- /dev/null +++ b/tests/softhsm_setup.py @@ -0,0 +1,328 @@ +"""Testing the PKCS#11 shim layer. + +Heavily inspired by from https://github.com/IdentityPython/pyXMLSecurity by leifj +under license "As is", see https://github.com/IdentityPython/pyXMLSecurity/blob/master/LICENSE.txt +""" + +import logging +import os +import shutil +import subprocess +import tempfile +import traceback +import unittest + +DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') + + +def paths_for_component(component: str, default_paths): + env_path = os.environ.get(component) + return [env_path] if env_path else default_paths + + +def find_alts(component_name, alts) -> str: + for a in alts: + if os.path.exists(a): + return a + raise unittest.SkipTest(f'Required component is missing: {component_name}') + + +def run_cmd(args, softhsm_conf=None): + env = {} + if softhsm_conf is not None: + env['SOFTHSM_CONF'] = softhsm_conf + env['SOFTHSM2_CONF'] = softhsm_conf + proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) + out, err = proc.communicate() + if err is not None and len(err) > 0: + logging.error(err) + if out is not None and len(out) > 0: + logging.debug(out) + rv = proc.wait() + if rv: + with open(softhsm_conf) as f: + conf = f.read() + msg = '[cmd: {cmd}] [code: {code}] [stdout: {out}] [stderr: {err}] [config: {conf}]' + msg = msg.format( + cmd=' '.join(args), + code=rv, + out=out.strip(), + err=err.strip(), + conf=conf, + ) + raise RuntimeError(msg) + return out, err + + +component_default_paths = { + 'P11_MODULE': [ + '/usr/lib/softhsm/libsofthsm2.so', + '/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so', + '/usr/lib/softhsm/libsofthsm.so', + '/usr/lib64/softhsm/libsofthsm2.so', + ], + 'P11_ENGINE': [ + '/usr/lib/ssl/engines/libpkcs11.so', + '/usr/lib/engines/engine_pkcs11.so', + '/usr/lib/x86_64-linux-gnu/engines-1.1/pkcs11.so', + '/usr/lib64/engines-1.1/pkcs11.so', + '/usr/lib64/engines-1.1/libpkcs11.so', + '/usr/lib64/engines-3/pkcs11.so', + '/usr/lib64/engines-3/libpkcs11.so', + '/usr/lib/x86_64-linux-gnu/engines-3/pkcs11.so', + '/usr/lib/x86_64-linux-gnu/engines-3/libpkcs11.so', + ], + 'PKCS11_TOOL': [ + '/usr/bin/pkcs11-tool', + ], + 'SOFTHSM': [ + '/usr/bin/softhsm2-util', + '/usr/bin/softhsm', + ], + 'OPENSSL': [ + '/usr/bin/openssl', + ], +} + +component_path = { + component_name: find_alts(component_name, paths_for_component(component_name, default_paths)) + for component_name, default_paths in component_default_paths.items() +} + +softhsm_version = 1 +if component_path['SOFTHSM'].endswith('softhsm2-util'): + softhsm_version = 2 + +openssl_version = subprocess.check_output([component_path['OPENSSL'], 'version'])[8:11].decode() + +p11_test_files = [] +softhsm_conf = None +softhsm_db = None + + +def _temp_file() -> str: + f = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 + p11_test_files.append(f.name) + return f.name + + +def _temp_dir() -> str: + d = tempfile.mkdtemp() + p11_test_files.append(d) + return d + + +@unittest.skipIf(component_path['P11_MODULE'] is None, 'SoftHSM PKCS11 module not installed') +def setup() -> None: + logging.debug('Creating test pkcs11 token using softhsm') + try: + global softhsm_conf + softhsm_conf = _temp_file() + logging.debug('Generating softhsm.conf') + with open(softhsm_conf, 'w') as f: + if softhsm_version == 2: + softhsm_db = _temp_dir() + f.write( + f""" +# Generated by test +directories.tokendir = {softhsm_db} +objectstore.backend = file +log.level = DEBUG +""" + ) + else: + softhsm_db = _temp_file() + f.write( + f""" +# Generated by test +0:{softhsm_db} +""" + ) + + logging.debug('Initializing the token') + _, _ = run_cmd( + [ + component_path['SOFTHSM'], + '--slot', + '0', + '--label', + 'test', + '--init-token', + '--pin', + 'secret1', + '--so-pin', + 'secret2', + ], + softhsm_conf=softhsm_conf, + ) + + hash_priv_key = _temp_file() + logging.debug('Converting test private key to format for softhsm') + run_cmd( + [ + component_path['OPENSSL'], + 'pkcs8', + '-topk8', + '-inform', + 'PEM', + '-outform', + 'PEM', + '-nocrypt', + '-in', + os.path.join(DATA_DIR, 'rsakey.pem'), + '-out', + hash_priv_key, + ], + softhsm_conf=softhsm_conf, + ) + + logging.debug('Importing the test key to softhsm') + run_cmd( + [ + component_path['SOFTHSM'], + '--import', + hash_priv_key, + '--token', + 'test', + '--id', + 'a1b2', + '--label', + 'test', + '--pin', + 'secret1', + ], + softhsm_conf=softhsm_conf, + ) + run_cmd( + [ + component_path['PKCS11_TOOL'], + '--module', + component_path['P11_MODULE'], + '-l', + '--pin', + 'secret1', + '-O', + ], + softhsm_conf=softhsm_conf, + ) + signer_cert_pem = _temp_file() + openssl_conf = _temp_file() + logging.debug('Generating OpenSSL config for version %s', openssl_version) + with open(openssl_conf, 'w') as f: + f.write( + '\n'.join( + [ + 'openssl_conf = openssl_def', + '[openssl_def]', + 'engines = engine_section', + '[engine_section]', + 'pkcs11 = pkcs11_section', + '[req]', + 'distinguished_name = req_distinguished_name', + '[req_distinguished_name]', + '[pkcs11_section]', + 'engine_id = pkcs11', + # dynamic_path, + 'MODULE_PATH = {}'.format(component_path['P11_MODULE']), + 'init = 0', + ] + ) + ) + + with open(openssl_conf) as f: + logging.debug('-------- START DEBUG openssl_conf --------') + logging.debug(f.readlines()) + logging.debug('-------- END DEBUG openssl_conf --------') + logging.debug('-------- START DEBUG paths --------') + logging.debug(run_cmd(['ls', '-ld', component_path['P11_ENGINE']])) + logging.debug(run_cmd(['ls', '-ld', component_path['P11_MODULE']])) + logging.debug('-------- END DEBUG paths --------') + + signer_cert_der = _temp_file() + + logging.debug('Generating self-signed certificate') + run_cmd( + [ + component_path['OPENSSL'], + 'req', + '-new', + '-x509', + '-subj', + '/CN=Test Signer', + '-engine', + 'pkcs11', + '-config', + openssl_conf, + '-keyform', + 'engine', + '-key', + 'label_test', + '-passin', + 'pass:secret1', + '-out', + signer_cert_pem, + ], + softhsm_conf=softhsm_conf, + ) + + run_cmd( + [ + component_path['OPENSSL'], + 'x509', + '-inform', + 'PEM', + '-outform', + 'DER', + '-in', + signer_cert_pem, + '-out', + signer_cert_der, + ], + softhsm_conf=softhsm_conf, + ) + + logging.debug('Importing certificate into token') + + run_cmd( + [ + component_path['PKCS11_TOOL'], + '--module', + component_path['P11_MODULE'], + '-l', + '--slot-index', + '0', + '--id', + 'a1b2', + '--label', + 'test', + '-y', + 'cert', + '-w', + signer_cert_der, + '--pin', + 'secret1', + ], + softhsm_conf=softhsm_conf, + ) + + # TODO: Should be teardowned in teardown + os.environ['SOFTHSM_CONF'] = softhsm_conf + os.environ['SOFTHSM2_CONF'] = softhsm_conf + + except Exception as ex: + print('-' * 64) + traceback.print_exc() + print('-' * 64) + logging.exception('PKCS11 tests disabled: unable to initialize test token') + raise ex + + +def teardown() -> None: + global p11_test_files + for o in p11_test_files: + if os.path.exists(o): + if os.path.isdir(o): + shutil.rmtree(o) + else: + os.unlink(o) + p11_test_files = [] diff --git a/tests/test_constants.py b/tests/test_constants.py index 857a1cdd..2c39d5f6 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -1,7 +1,8 @@ """Test constants from :mod:`xmlsec.constants` module.""" +import pytest + import xmlsec -from hypothesis import given, strategies def _constants(typename): @@ -17,25 +18,25 @@ def _constants(typename): ) -@given(transform=strategies.sampled_from(_constants('__Transform'))) +@pytest.mark.parametrize('transform', _constants('__Transform'), ids=repr) def test_transform_str(transform): """Test string representation of ``xmlsec.constants.__Transform``.""" - assert str(transform) == '{}, {}'.format(transform.name, transform.href) + assert str(transform) == f'{transform.name}, {transform.href}' -@given(transform=strategies.sampled_from(_constants('__Transform'))) +@pytest.mark.parametrize('transform', _constants('__Transform'), ids=repr) def test_transform_repr(transform): """Test raw string representation of ``xmlsec.constants.__Transform``.""" - assert repr(transform) == '__Transform({!r}, {!r}, {})'.format(transform.name, transform.href, transform.usage) + assert repr(transform) == f'__Transform({transform.name!r}, {transform.href!r}, {transform.usage})' -@given(keydata=strategies.sampled_from(_constants('__KeyData'))) +@pytest.mark.parametrize('keydata', _constants('__KeyData'), ids=repr) def test_keydata_str(keydata): """Test string representation of ``xmlsec.constants.__KeyData``.""" - assert str(keydata) == '{}, {}'.format(keydata.name, keydata.href) + assert str(keydata) == f'{keydata.name}, {keydata.href}' -@given(keydata=strategies.sampled_from(_constants('__KeyData'))) +@pytest.mark.parametrize('keydata', _constants('__KeyData'), ids=repr) def test_keydata_repr(keydata): """Test raw string representation of ``xmlsec.constants.__KeyData``.""" - assert repr(keydata) == '__KeyData({!r}, {!r})'.format(keydata.name, keydata.href) + assert repr(keydata) == f'__KeyData({keydata.name!r}, {keydata.href!r})' diff --git a/tests/test_doc_examples.py b/tests/test_doc_examples.py index 2fc490f3..7aa8e517 100644 --- a/tests/test_doc_examples.py +++ b/tests/test_doc_examples.py @@ -3,24 +3,17 @@ import contextlib import os import runpy -import sys +from pathlib import Path import pytest -if sys.version_info >= (3, 4): - from pathlib import Path -else: # python2.7 compat - from _pytest.pathlib import Path - - examples_dir = Path(__file__, '../../doc/source/examples').resolve() examples = sorted(examples_dir.glob('*.py')) @contextlib.contextmanager def cd(where_to): - """ - Temporarily change the working directory. + """Temporarily change the working directory. Restore the current working dir after exiting the context. """ @@ -34,8 +27,7 @@ def cd(where_to): @pytest.mark.parametrize('example', examples, ids=lambda p: p.name) def test_doc_example(example): - """ - Verify example scripts included in the docs are up to date. + """Verify example scripts included in the docs are up to date. Execute each script in :file:`docs/source/examples`, not raising any errors is good enough. diff --git a/tests/test_ds.py b/tests/test_ds.py index 26d49075..dd0657d3 100644 --- a/tests/test_ds.py +++ b/tests/test_ds.py @@ -1,7 +1,7 @@ -from tests import base +import unittest import xmlsec - +from tests import base consts = xmlsec.constants @@ -11,36 +11,87 @@ def test_init(self): ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) del ctx - def test_key(self): + def test_init_no_keys_manager(self): + ctx = xmlsec.SignatureContext() + del ctx + + def test_init_bad_args(self): + with self.assertRaisesRegex(TypeError, 'KeysManager required'): + xmlsec.SignatureContext(manager='foo') + + def test_no_key(self): ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) self.assertIsNone(ctx.key) - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + + def test_del_key(self): + ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + del ctx.key + self.assertIsNone(ctx.key) + + def test_set_key(self): + ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) + def test_set_key_bad_type(self): + ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) + with self.assertRaisesRegex(TypeError, r'instance of \*xmlsec.Key\* expected.'): + ctx.key = '' + + def test_set_invalid_key(self): + ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) + with self.assertRaisesRegex(TypeError, 'empty key.'): + ctx.key = xmlsec.Key() + def test_register_id(self): ctx = xmlsec.SignatureContext() - root = self.load_xml("sign_template.xml") - sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, "Id") - ctx.register_id(sign, "Id") + root = self.load_xml('sign_template.xml') + sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, 'Id') + ctx.register_id(sign, 'Id') + + def test_register_id_bad_args(self): + ctx = xmlsec.SignatureContext() + with self.assertRaises(TypeError): + ctx.register_id('') + + def test_register_id_with_namespace_without_attribute(self): + ctx = xmlsec.SignatureContext() + root = self.load_xml('sign_template.xml') + sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, 'Id') + with self.assertRaisesRegex(xmlsec.Error, 'missing attribute.'): + ctx.register_id(sign, 'Id', id_ns='foo') + + def test_sign_bad_args(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + with self.assertRaises(TypeError): + ctx.sign('') + + def test_sign_fail(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + with self.assertRaisesRegex(xmlsec.Error, 'failed to sign'): + ctx.sign(self.load_xml('sign1-in.xml')) def test_sign_case1(self): """Should sign a pre-constructed template file using a key from a PEM file.""" - root = self.load_xml("sign1-in.xml") + root = self.load_xml('sign1-in.xml') sign = xmlsec.tree.find_node(root, consts.NodeSignature) self.assertIsNotNone(sign) ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) - self.assertEqual(self.load_xml("sign1-out.xml"), root) + self.assertEqual(self.load_xml('sign1-out.xml'), root) def test_sign_case2(self): """Should sign a dynamicaly constructed template file using a key from a PEM file.""" - root = self.load_xml("sign2-in.xml") + root = self.load_xml('sign2-in.xml') sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1) self.assertIsNotNone(sign) root.append(sign) @@ -50,17 +101,17 @@ def test_sign_case2(self): xmlsec.template.add_key_name(ki) ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) - self.assertEqual(self.load_xml("sign2-out.xml"), root) + self.assertEqual(self.load_xml('sign2-out.xml'), root) def test_sign_case3(self): """Should sign a file using a dynamicaly created template, key from PEM and an X509 cert.""" - root = self.load_xml("sign3-in.xml") + root = self.load_xml('sign3-in.xml') sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1) self.assertIsNotNone(sign) root.append(sign) @@ -70,24 +121,23 @@ def test_sign_case3(self): xmlsec.template.add_x509_data(ki) ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.load_cert_from_file(self.path('rsacert.pem'), consts.KeyDataFormatPem) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) - self.assertEqual(self.load_xml("sign3-out.xml"), root) + self.assertEqual(self.load_xml('sign3-out.xml'), root) def test_sign_case4(self): """Should sign a file using a dynamically created template, key from PEM and an X509 cert with custom ns.""" - - root = self.load_xml("sign4-in.xml") - xmlsec.tree.add_ids(root, ["ID"]) + root = self.load_xml('sign4-in.xml') + xmlsec.tree.add_ids(root, ['ID']) elem_id = root.get('ID', None) if elem_id: elem_id = '#' + elem_id - sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, ns="ds") + sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, ns='ds') self.assertIsNotNone(sign) root.append(sign) ref = xmlsec.template.add_reference(sign, consts.TransformSha1, uri=elem_id) @@ -97,18 +147,18 @@ def test_sign_case4(self): xmlsec.template.add_x509_data(ki) ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.load_cert_from_file(self.path('rsacert.pem'), consts.KeyDataFormatPem) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) - self.assertEqual(self.load_xml("sign4-out.xml"), root) + self.assertEqual(self.load_xml('sign4-out.xml'), root) def test_sign_case5(self): """Should sign a file using a dynamicaly created template, key from PEM file and an X509 certificate.""" - root = self.load_xml("sign5-in.xml") + root = self.load_xml('sign5-in.xml') sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1) self.assertIsNotNone(sign) root.append(sign) @@ -125,24 +175,66 @@ def test_sign_case5(self): xmlsec.template.x509_issuer_serial_add_serial_number(x509_issuer_serial, '1') ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.load_cert_from_file(self.path('rsacert.pem'), consts.KeyDataFormatPem) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) - self.assertEqual(self.load_xml("sign5-out.xml"), root) + if (1, 2, 36) <= xmlsec.get_libxmlsec_version() <= (1, 2, 37): + expected_xml_file = 'sign5-out-xmlsec_1_2_36_to_37.xml' + else: + expected_xml_file = 'sign5-out.xml' + self.assertEqual(self.load_xml(expected_xml_file), root) + + def test_sign_binary_bad_args(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + with self.assertRaises(TypeError): + ctx.sign_binary(bytes=1, transform='') + + def test_sign_binary_no_key(self): + ctx = xmlsec.SignatureContext() + with self.assertRaisesRegex(xmlsec.Error, 'Sign key is not specified.'): + ctx.sign_binary(bytes=b'', transform=consts.TransformRsaSha1) + + @unittest.skipIf(not hasattr(consts, 'TransformXslt'), reason='XSLT transformations not enabled') + def test_sign_binary_invalid_signature_method(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + with self.assertRaisesRegex(xmlsec.Error, 'incompatible signature method'): + ctx.sign_binary(bytes=b'', transform=consts.TransformXslt) def test_sign_binary(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) + + sign = ctx.sign_binary(self.load('sign6-in.bin'), consts.TransformRsaSha1) + self.assertEqual(self.load('sign6-out.bin'), sign) - sign = ctx.sign_binary(self.load("sign6-in.bin"), consts.TransformRsaSha1) - self.assertEqual(self.load("sign6-out.bin"), sign) + def test_sign_binary_twice_not_possible(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + data = self.load('sign6-in.bin') + ctx.sign_binary(data, consts.TransformRsaSha1) + with self.assertRaisesRegex(xmlsec.Error, 'Signature context already used; it is designed for one use only.'): + ctx.sign_binary(data, consts.TransformRsaSha1) + + def test_verify_bad_args(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + with self.assertRaises(TypeError): + ctx.verify('') + + def test_verify_fail(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + with self.assertRaisesRegex(xmlsec.Error, 'failed to verify'): + ctx.verify(self.load_xml('sign1-in.xml')) def test_verify_case_1(self): self.check_verify(1) @@ -160,34 +252,86 @@ def test_verify_case_5(self): self.check_verify(5) def check_verify(self, i): - root = self.load_xml("sign%d-out.xml" % i) - xmlsec.tree.add_ids(root, ["ID"]) + root = self.load_xml(f'sign{i}-out.xml') + xmlsec.tree.add_ids(root, ['ID']) sign = xmlsec.tree.find_node(root, consts.NodeSignature) self.assertIsNotNone(sign) - self.assertEqual(consts.NodeSignature, sign.tag.partition("}")[2]) + self.assertEqual(consts.NodeSignature, sign.tag.partition('}')[2]) ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsapub.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsapub.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsapub.pem' - self.assertEqual("rsapub.pem", ctx.key.name) + self.assertEqual('rsapub.pem', ctx.key.name) ctx.verify(sign) def test_validate_binary_sign(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) - ctx.verify_binary(self.load("sign6-in.bin"), consts.TransformRsaSha1, self.load("sign6-out.bin")) + ctx.verify_binary(self.load('sign6-in.bin'), consts.TransformRsaSha1, self.load('sign6-out.bin')) def test_validate_binary_sign_fail(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) with self.assertRaises(xmlsec.Error): - ctx.verify_binary(self.load("sign6-in.bin"), consts.TransformRsaSha1, b"invalid") + ctx.verify_binary(self.load('sign6-in.bin'), consts.TransformRsaSha1, b'invalid') + + def test_enable_reference_transform(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + ctx.enable_reference_transform(consts.TransformRsaSha1) + + def test_enable_reference_transform_bad_args(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + with self.assertRaises(TypeError): + ctx.enable_reference_transform('') + with self.assertRaises(TypeError): + ctx.enable_reference_transform(0) + with self.assertRaises(TypeError): + ctx.enable_reference_transform(consts.KeyDataAes) + + def test_enable_signature_transform(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + ctx.enable_signature_transform(consts.TransformRsaSha1) + + def test_enable_signature_transform_bad_args(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + with self.assertRaises(TypeError): + ctx.enable_signature_transform('') + with self.assertRaises(TypeError): + ctx.enable_signature_transform(0) + with self.assertRaises(TypeError): + ctx.enable_signature_transform(consts.KeyDataAes) + + def test_set_enabled_key_data(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + ctx.set_enabled_key_data([consts.KeyDataAes]) + + def test_set_enabled_key_data_empty(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + ctx.set_enabled_key_data([]) + + def test_set_enabled_key_data_bad_args(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + with self.assertRaises(TypeError): + ctx.set_enabled_key_data(0) + + def test_set_enabled_key_data_bad_list(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + with self.assertRaisesRegex(TypeError, 'expected list of KeyData constants.'): + ctx.set_enabled_key_data('foo') diff --git a/tests/test_enc.py b/tests/test_enc.py index 2f8c9d74..41f78d74 100644 --- a/tests/test_enc.py +++ b/tests/test_enc.py @@ -1,7 +1,9 @@ -from tests import base +import tempfile -import xmlsec +from lxml import etree +import xmlsec +from tests import base consts = xmlsec.constants @@ -11,26 +13,57 @@ def test_init(self): ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) del ctx - def test_key(self): + def test_init_no_keys_manager(self): + ctx = xmlsec.EncryptionContext() + del ctx + + def test_init_bad_args(self): + with self.assertRaisesRegex(TypeError, 'KeysManager required'): + xmlsec.EncryptionContext(manager='foo') + + def test_no_key(self): ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) self.assertIsNone(ctx.key) - ctx.key = xmlsec.Key.from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatCertPem) + + def test_get_key(self): + ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) + self.assertIsNone(ctx.key) + ctx.key = xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem) self.assertIsNotNone(ctx.key) + def test_del_key(self): + ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) + ctx.key = xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem) + del ctx.key + self.assertIsNone(ctx.key) + + def test_set_key(self): + ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) + ctx.key = xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem) + self.assertIsNotNone(ctx.key) + + def test_set_key_bad_type(self): + ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) + with self.assertRaisesRegex(TypeError, r'instance of \*xmlsec.Key\* expected.'): + ctx.key = '' + + def test_set_invalid_key(self): + ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) + with self.assertRaisesRegex(TypeError, 'empty key.'): + ctx.key = xmlsec.Key() + def test_encrypt_xml(self): root = self.load_xml('enc1-in.xml') - enc_data = xmlsec.template.encrypted_data_create( - root, consts.TransformAes128Cbc, type=consts.TypeEncElement, ns="xenc" - ) + enc_data = xmlsec.template.encrypted_data_create(root, consts.TransformAes128Cbc, type=consts.TypeEncElement, ns='xenc') xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) - ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns="dsig") + ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns='dsig') ek = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep) xmlsec.template.encrypted_data_ensure_cipher_value(ek) data = root.find('./Data') self.assertIsNotNone(data) manager = xmlsec.KeysManager() - manager.add_key(xmlsec.Key.from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatCertPem)) + manager.add_key(xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem)) ctx = xmlsec.EncryptionContext(manager) ctx.key = xmlsec.Key.generate(consts.KeyDataAes, 128, consts.KeyDataTypeSession) @@ -40,47 +73,126 @@ def test_encrypt_xml(self): enc_method = xmlsec.tree.find_child(enc_data, consts.NodeEncryptionMethod, consts.EncNs) self.assertIsNotNone(enc_method) - self.assertEqual("http://www.w3.org/2001/04/xmlenc#aes128-cbc", enc_method.get("Algorithm")) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#aes128-cbc', enc_method.get('Algorithm')) ki = xmlsec.tree.find_child(enc_data, consts.NodeKeyInfo, consts.DSigNs) self.assertIsNotNone(ki) enc_method2 = xmlsec.tree.find_node(ki, consts.NodeEncryptionMethod, consts.EncNs) self.assertIsNotNone(enc_method2) - self.assertEqual("http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p", enc_method2.get("Algorithm")) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p', enc_method2.get('Algorithm')) cipher_value = xmlsec.tree.find_node(ki, consts.NodeCipherValue, consts.EncNs) self.assertIsNotNone(cipher_value) + def test_encrypt_xml_bad_args(self): + ctx = xmlsec.EncryptionContext() + with self.assertRaises(TypeError): + ctx.encrypt_xml('', 0) + + def test_encrypt_xml_bad_template(self): + ctx = xmlsec.EncryptionContext() + with self.assertRaisesRegex(xmlsec.Error, 'unsupported `Type`, it should be `element` or `content`'): + ctx.encrypt_xml(etree.Element('root'), etree.Element('node')) + + def test_encrypt_xml_bad_template_bad_type_attribute(self): + ctx = xmlsec.EncryptionContext() + with self.assertRaisesRegex(xmlsec.Error, 'unsupported `Type`, it should be `element` or `content`'): + root = etree.Element('root') + root.attrib['Type'] = 'foo' + ctx.encrypt_xml(root, etree.Element('node')) + + def test_encrypt_xml_fail(self): + ctx = xmlsec.EncryptionContext() + with self.assertRaisesRegex(xmlsec.Error, 'failed to encrypt xml'): + root = etree.Element('root') + root.attrib['Type'] = consts.TypeEncElement + ctx.encrypt_xml(root, etree.Element('node')) + def test_encrypt_binary(self): root = self.load_xml('enc2-in.xml') enc_data = xmlsec.template.encrypted_data_create( - root, consts.TransformAes128Cbc, type=consts.TypeEncContent, ns="xenc", mime_type="binary/octet-stream" + root, consts.TransformAes128Cbc, type=consts.TypeEncContent, ns='xenc', mime_type='binary/octet-stream' ) xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) - ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns="dsig") + ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns='dsig') ek = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep) xmlsec.template.encrypted_data_ensure_cipher_value(ek) manager = xmlsec.KeysManager() - manager.add_key(xmlsec.Key.from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatCertPem)) + manager.add_key(xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem)) ctx = xmlsec.EncryptionContext(manager) ctx.key = xmlsec.Key.generate(consts.KeyDataAes, 128, consts.KeyDataTypeSession) encrypted = ctx.encrypt_binary(enc_data, b'test') self.assertIsNotNone(encrypted) - self.assertEqual("{%s}%s" % (consts.EncNs, consts.NodeEncryptedData), encrypted.tag) + self.assertEqual(f'{{{consts.EncNs}}}{consts.NodeEncryptedData}', encrypted.tag) enc_method = xmlsec.tree.find_child(enc_data, consts.NodeEncryptionMethod, consts.EncNs) self.assertIsNotNone(enc_method) - self.assertEqual("http://www.w3.org/2001/04/xmlenc#aes128-cbc", enc_method.get("Algorithm")) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#aes128-cbc', enc_method.get('Algorithm')) ki = xmlsec.tree.find_child(enc_data, consts.NodeKeyInfo, consts.DSigNs) self.assertIsNotNone(ki) enc_method2 = xmlsec.tree.find_node(ki, consts.NodeEncryptionMethod, consts.EncNs) self.assertIsNotNone(enc_method2) - self.assertEqual("http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p", enc_method2.get("Algorithm")) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p', enc_method2.get('Algorithm')) cipher_value = xmlsec.tree.find_node(ki, consts.NodeCipherValue, consts.EncNs) self.assertIsNotNone(cipher_value) + def test_encrypt_binary_bad_args(self): + ctx = xmlsec.EncryptionContext() + with self.assertRaises(TypeError): + ctx.encrypt_binary('', 0) + + def test_encrypt_binary_bad_template(self): + ctx = xmlsec.EncryptionContext() + with self.assertRaisesRegex(xmlsec.Error, 'failed to encrypt binary'): + ctx.encrypt_binary(etree.Element('root'), b'data') + + def test_encrypt_uri(self): + root = self.load_xml('enc2-in.xml') + enc_data = xmlsec.template.encrypted_data_create( + root, consts.TransformAes128Cbc, type=consts.TypeEncContent, ns='xenc', mime_type='binary/octet-stream' + ) + xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) + ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns='dsig') + ek = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep) + xmlsec.template.encrypted_data_ensure_cipher_value(ek) + + manager = xmlsec.KeysManager() + manager.add_key(xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem)) + + ctx = xmlsec.EncryptionContext(manager) + ctx.key = xmlsec.Key.generate(consts.KeyDataAes, 128, consts.KeyDataTypeSession) + + with tempfile.NamedTemporaryFile(delete=False) as tmpfile: + tmpfile.write(b'test') + + encrypted = ctx.encrypt_binary(enc_data, 'file://' + tmpfile.name) + self.assertIsNotNone(encrypted) + self.assertEqual(f'{{{consts.EncNs}}}{consts.NodeEncryptedData}', encrypted.tag) + + enc_method = xmlsec.tree.find_child(enc_data, consts.NodeEncryptionMethod, consts.EncNs) + self.assertIsNotNone(enc_method) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#aes128-cbc', enc_method.get('Algorithm')) + + ki = xmlsec.tree.find_child(enc_data, consts.NodeKeyInfo, consts.DSigNs) + self.assertIsNotNone(ki) + enc_method2 = xmlsec.tree.find_node(ki, consts.NodeEncryptionMethod, consts.EncNs) + self.assertIsNotNone(enc_method2) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p', enc_method2.get('Algorithm')) + cipher_value = xmlsec.tree.find_node(ki, consts.NodeCipherValue, consts.EncNs) + self.assertIsNotNone(cipher_value) + + def test_encrypt_uri_bad_args(self): + ctx = xmlsec.EncryptionContext() + with self.assertRaises(TypeError): + ctx.encrypt_uri('', 0) + + def test_encrypt_uri_fail(self): + ctx = xmlsec.EncryptionContext() + with self.assertRaisesRegex(xmlsec.Error, 'failed to encrypt URI'): + ctx.encrypt_uri(etree.Element('root'), '') + def test_decrypt1(self): self.check_decrypt(1) @@ -93,7 +205,7 @@ def test_decrypt_key(self): self.assertIsNotNone(enc_key) manager = xmlsec.KeysManager() - manager.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) + manager.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) ctx = xmlsec.EncryptionContext(manager) keydata = ctx.decrypt(enc_key) ctx.reset() @@ -103,37 +215,21 @@ def test_decrypt_key(self): self.assertIsNotNone(enc_data) decrypted = ctx.decrypt(enc_data) self.assertIsNotNone(decrypted) - self.assertEqual(self.load_xml("enc3-in.xml"), decrypted) + self.assertEqual(self.load_xml('enc3-in.xml'), decrypted) def check_decrypt(self, i): - root = self.load_xml('enc%d-out.xml' % i) + root = self.load_xml(f'enc{i}-out.xml') enc_data = xmlsec.tree.find_child(root, consts.NodeEncryptedData, consts.EncNs) self.assertIsNotNone(enc_data) manager = xmlsec.KeysManager() - manager.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) + manager.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) ctx = xmlsec.EncryptionContext(manager) decrypted = ctx.decrypt(enc_data) self.assertIsNotNone(decrypted) - self.assertEqual(self.load_xml("enc%d-in.xml" % i), root) - - - def check_no_segfault(self): - namespaces = { - 'soap': 'http://schemas.xmlsoap.org/soap/envelope/' - } + self.assertEqual(self.load_xml(f'enc{i}-in.xml'), root) - manager = xmlsec.KeysManager() - key = xmlsec.Key.from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatCertPem) - manager.add_key(key) - template = self.load_xml('enc-bad-in.xml') - enc_data = xmlsec.template.encrypted_data_create( - template, xmlsec.Transform.AES128, type=xmlsec.EncryptionType.CONTENT, ns='xenc') - xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) - key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns='dsig') - enc_key = xmlsec.template.add_encrypted_key(key_info, xmlsec.Transform.RSA_PKCS1) - xmlsec.template.encrypted_data_ensure_cipher_value(enc_key) - data = template.find('soap:Body', namespaces=namespaces) - enc_ctx = xmlsec.EncryptionContext(manager) - enc_ctx.key = xmlsec.Key.generate(xmlsec.KeyData.AES, 192, xmlsec.KeyDataType.SESSION) - self.assertRaises(Exception, enc_ctx.encrypt_xml(enc_data, data)) + def test_decrypt_bad_args(self): + ctx = xmlsec.EncryptionContext() + with self.assertRaises(TypeError): + ctx.decrypt('') diff --git a/tests/test_keys.py b/tests/test_keys.py index add11e41..977ddf82 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -1,132 +1,218 @@ -from tests import base - import copy +import tempfile import xmlsec - +from tests import base consts = xmlsec.constants class TestKeys(base.TestMemoryLeaks): def test_key_from_memory(self): - key = xmlsec.Key.from_memory(self.load("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_memory(self.load('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) def test_key_from_memory_with_bad_args(self): with self.assertRaises(TypeError): - xmlsec.Key.from_memory(1, format="") + xmlsec.Key.from_memory(1, format='') + + def test_key_from_memory_invalid_data(self): + with self.assertRaisesRegex(xmlsec.Error, '.*cannot load key.*'): + xmlsec.Key.from_memory(b'foo', format=consts.KeyDataFormatPem) def test_key_from_file(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) def test_key_from_file_with_bad_args(self): with self.assertRaises(TypeError): - xmlsec.Key.from_file(1, format="") + xmlsec.Key.from_file(1, format='') + + def test_key_from_invalid_file(self): + with self.assertRaisesRegex(xmlsec.Error, '.*cannot read key.*'), tempfile.NamedTemporaryFile() as tmpfile: + tmpfile.write(b'foo') + xmlsec.Key.from_file(tmpfile.name, format=consts.KeyDataFormatPem) def test_key_from_fileobj(self): - with open(self.path("rsakey.pem"), "rb") as fobj: + with open(self.path('rsakey.pem'), 'rb') as fobj: key = xmlsec.Key.from_file(fobj, format=consts.KeyDataFormatPem) self.assertIsNotNone(key) + def test_key_from_invalid_fileobj(self): + with tempfile.NamedTemporaryFile(delete=False) as tmpfile: + tmpfile.write(b'foo') + with self.assertRaisesRegex(xmlsec.Error, '.*cannot read key.*'), open(tmpfile.name) as fp: + xmlsec.Key.from_file(fp, format=consts.KeyDataFormatPem) + def test_generate(self): key = xmlsec.Key.generate(klass=consts.KeyDataAes, size=256, type=consts.KeyDataTypeSession) self.assertIsNotNone(key) def test_generate_with_bad_args(self): with self.assertRaises(TypeError): - xmlsec.Key.generate(klass="", size="", type="") + xmlsec.Key.generate(klass='', size='', type='') + + def test_generate_invalid_size(self): + with self.assertRaisesRegex(xmlsec.Error, '.*cannot generate key.*'): + xmlsec.Key.generate(klass=consts.KeyDataAes, size=0, type=consts.KeyDataTypeSession) def test_from_binary_file(self): - key = xmlsec.Key.from_binary_file(klass=consts.KeyDataDes, filename=self.path("deskey.bin")) + key = xmlsec.Key.from_binary_file(klass=consts.KeyDataDes, filename=self.path('deskey.bin')) self.assertIsNotNone(key) def test_from_binary_file_with_bad_args(self): with self.assertRaises(TypeError): - xmlsec.Key.from_binary_file(klass="", filename=1) + xmlsec.Key.from_binary_file(klass='', filename=1) + + def test_from_invalid_binary_file(self): + with self.assertRaisesRegex(xmlsec.Error, '.*cannot read key.*'), tempfile.NamedTemporaryFile() as tmpfile: + tmpfile.write(b'foo') + xmlsec.Key.from_binary_file(klass=consts.KeyDataDes, filename=tmpfile.name) def test_from_binary_data(self): - key = xmlsec.Key.from_binary_data(klass=consts.KeyDataDes, data=self.load("deskey.bin")) + key = xmlsec.Key.from_binary_data(klass=consts.KeyDataDes, data=self.load('deskey.bin')) self.assertIsNotNone(key) def test_from_binary_data_with_bad_args(self): with self.assertRaises(TypeError): - xmlsec.Key.from_binary_data(klass="", data=1) + xmlsec.Key.from_binary_data(klass='', data=1) + + def test_from_invalid_binary_data(self): + with self.assertRaisesRegex(xmlsec.Error, '.*cannot read key.*'): + xmlsec.Key.from_binary_data(klass=consts.KeyDataDes, data=b'') def test_load_cert_from_file(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) - key.load_cert_from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatPem) + key.load_cert_from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatPem) def test_load_cert_from_file_with_bad_args(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) with self.assertRaises(TypeError): - key.load_cert_from_file(1, format="") + key.load_cert_from_file(1, format='') + + def test_load_cert_from_invalid_file(self): + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + self.assertIsNotNone(key) + with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'), tempfile.NamedTemporaryFile() as tmpfile: + tmpfile.write(b'foo') + key.load_cert_from_file(tmpfile.name, format=consts.KeyDataFormatPem) def test_load_cert_from_fileobj(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) - with open(self.path("rsacert.pem"), "rb") as fobj: + with open(self.path('rsacert.pem'), 'rb') as fobj: key.load_cert_from_file(fobj, format=consts.KeyDataFormatPem) + def test_load_cert_from_fileobj_with_bad_args(self): + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + self.assertIsNotNone(key) + with self.assertRaises(TypeError), open(self.path('rsacert.pem'), 'rb') as fobj: + key.load_cert_from_file(fobj, format='') + + def test_load_cert_from_invalid_fileobj(self): + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + self.assertIsNotNone(key) + with tempfile.NamedTemporaryFile(delete=False) as tmpfile: + tmpfile.write(b'foo') + with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'), open(tmpfile.name) as fp: + key.load_cert_from_file(fp, format=consts.KeyDataFormatPem) + def test_load_cert_from_memory(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) - key.load_cert_from_memory(self.load("rsacert.pem"), format=consts.KeyDataFormatPem) + key.load_cert_from_memory(self.load('rsacert.pem'), format=consts.KeyDataFormatPem) def test_load_cert_from_memory_with_bad_args(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) with self.assertRaises(TypeError): - key.load_cert_from_memory(1, format="") + key.load_cert_from_memory(1, format='') + + def test_load_cert_from_memory_invalid_data(self): + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + self.assertIsNotNone(key) + with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'): + key.load_cert_from_memory(b'', format=consts.KeyDataFormatPem) + + def test_get_name(self): + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + self.assertIsNone(key.name) + + def test_get_name_invalid_key(self): + key = xmlsec.Key() + with self.assertRaisesRegex(ValueError, 'key is not ready'): + key.name # noqa: B018 - def test_name(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + def test_del_name(self): + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + key.name = 'rsakey' + del key.name self.assertIsNone(key.name) - key.name = "rsakey" - self.assertEqual("rsakey", key.name) + + def test_set_name(self): + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + key.name = 'rsakey' + self.assertEqual('rsakey', key.name) + + def test_set_name_invalid_key(self): + key = xmlsec.Key() + with self.assertRaisesRegex(ValueError, 'key is not ready'): + key.name = 'foo' def test_copy(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) key2 = copy.copy(key) del key - key2.load_cert_from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatPem) + key2.load_cert_from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatPem) class TestKeysManager(base.TestMemoryLeaks): def test_add_key(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) mngr = xmlsec.KeysManager() mngr.add_key(key) def test_add_key_with_bad_args(self): mngr = xmlsec.KeysManager() with self.assertRaises(TypeError): - mngr.add_key("") + mngr.add_key('') def test_load_cert(self): mngr = xmlsec.KeysManager() - mngr.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) - mngr.load_cert(self.path("rsacert.pem"), format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) + mngr.load_cert(self.path('rsacert.pem'), format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) def test_load_cert_with_bad_args(self): mngr = xmlsec.KeysManager() - mngr.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) + with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'), tempfile.NamedTemporaryFile() as tmpfile: + tmpfile.write(b'foo') + mngr.load_cert(tmpfile.name, format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) + + def test_load_invalid_cert(self): + mngr = xmlsec.KeysManager() + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) with self.assertRaises(TypeError): - mngr.load_cert(1, format="", type="") + mngr.load_cert(1, format='', type='') def test_load_cert_from_memory(self): mngr = xmlsec.KeysManager() - mngr.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) - mngr.load_cert_from_memory(self.load("rsacert.pem"), format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) + mngr.load_cert_from_memory(self.load('rsacert.pem'), format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) def test_load_cert_from_memory_with_bad_args(self): mngr = xmlsec.KeysManager() - mngr.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) with self.assertRaises(TypeError): - mngr.load_cert_from_memory(1, format="", type="") + mngr.load_cert_from_memory(1, format='', type='') + + def test_load_cert_from_memory_invalid_data(self): + mngr = xmlsec.KeysManager() + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) + with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'): + mngr.load_cert_from_memory(b'', format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) def test_load_invalid_key(self): mngr = xmlsec.KeysManager() diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 00000000..8f1501f2 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,160 @@ +import sys +from io import BytesIO +from unittest import skipIf + +import xmlsec +from tests import base +from xmlsec import constants as consts + + +class TestBase64LineSize(base.TestMemoryLeaks): + def tearDown(self): + xmlsec.base64_default_line_size(64) + super().tearDown() + + def test_get_base64_default_line_size(self): + self.assertEqual(xmlsec.base64_default_line_size(), 64) + + def test_set_base64_default_line_size_positional_arg(self): + xmlsec.base64_default_line_size(0) + self.assertEqual(xmlsec.base64_default_line_size(), 0) + + def test_set_base64_default_line_size_keyword_arg(self): + xmlsec.base64_default_line_size(size=0) + self.assertEqual(xmlsec.base64_default_line_size(), 0) + + def test_set_base64_default_line_size_with_bad_args(self): + size = xmlsec.base64_default_line_size() + for bad_size in (None, '', object()): + with self.assertRaises(TypeError): + xmlsec.base64_default_line_size(bad_size) + self.assertEqual(xmlsec.base64_default_line_size(), size) + + def test_set_base64_default_line_size_rejects_negative_values(self): + size = xmlsec.base64_default_line_size() + with self.assertRaises(ValueError): + xmlsec.base64_default_line_size(-1) + self.assertEqual(xmlsec.base64_default_line_size(), size) + + +class TestCallbacks(base.TestMemoryLeaks): + def setUp(self): + super().setUp() + xmlsec.cleanup_callbacks() + + def _sign_doc(self): + root = self.load_xml('doc.xml') + sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) + xmlsec.template.add_reference(sign, consts.TransformSha1, uri='cid:123456') + + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + ctx.sign(sign) + return sign + + def _expect_sign_failure(self): + with self.assertRaisesRegex(xmlsec.Error, 'failed to sign'): + self._sign_doc() + + def _mismatch_callbacks(self, match_cb=lambda filename: False): + return [ + match_cb, + lambda filename: None, + lambda none, buf: 0, + lambda none: None, + ] + + def _register_mismatch_callbacks(self, match_cb=lambda filename: False): + xmlsec.register_callbacks(*self._mismatch_callbacks(match_cb)) + + def _register_match_callbacks(self): + xmlsec.register_callbacks( + lambda filename: filename == b'cid:123456', + lambda filename: BytesIO(b''), + lambda bio, buf: bio.readinto(buf), + lambda bio: bio.close(), + ) + + def _find(self, elem, *tags): + try: + return elem.xpath( + './' + '/'.join(f'xmldsig:{tag}' for tag in tags), + namespaces={ + 'xmldsig': 'http://www.w3.org/2000/09/xmldsig#', + }, + )[0] + except IndexError as e: + raise KeyError(tags) from e + + def _verify_external_data_signature(self): + signature = self._sign_doc() + digest = self._find(signature, 'SignedInfo', 'Reference', 'DigestValue').text + self.assertEqual(digest, 'VihZwVMGJ48NsNl7ertVHiURXk8=') + + def test_sign_external_data_no_callbacks_fails(self): + self._expect_sign_failure() + + def test_sign_external_data_default_callbacks_fails(self): + xmlsec.register_default_callbacks() + self._expect_sign_failure() + + def test_sign_external_data_no_matching_callbacks_fails(self): + self._register_mismatch_callbacks() + self._expect_sign_failure() + + def test_sign_data_from_callbacks(self): + self._register_match_callbacks() + self._verify_external_data_signature() + + def test_sign_data_not_first_callback(self): + bad_match_calls = 0 + + def match_cb(filename): + nonlocal bad_match_calls + bad_match_calls += 1 + return False + + for _ in range(2): + self._register_mismatch_callbacks(match_cb) + + self._register_match_callbacks() + + for _ in range(2): + self._register_mismatch_callbacks() + + self._verify_external_data_signature() + self.assertEqual(bad_match_calls, 0) + + @skipIf(sys.platform == 'win32', 'unclear behaviour on windows') + def test_failed_sign_because_default_callbacks(self): + mismatch_calls = 0 + + def mismatch_cb(filename): + nonlocal mismatch_calls + mismatch_calls += 1 + return False + + # NB: These first two sets of callbacks should never get called, + # because the default callbacks always match beforehand: + self._register_match_callbacks() + self._register_mismatch_callbacks(mismatch_cb) + xmlsec.register_default_callbacks() + self._register_mismatch_callbacks(mismatch_cb) + self._register_mismatch_callbacks(mismatch_cb) + self._expect_sign_failure() + self.assertEqual(mismatch_calls, 2) + + def test_register_non_callables(self): + for idx in range(4): + cbs = self._mismatch_callbacks() + cbs[idx] = None + self.assertRaises(TypeError, xmlsec.register_callbacks, *cbs) + + def test_sign_external_data_fails_on_read_callback_wrong_returns(self): + xmlsec.register_callbacks( + lambda filename: filename == b'cid:123456', + lambda filename: BytesIO(b''), + lambda bio, buf: None, + lambda bio: bio.close(), + ) + self._expect_sign_failure() diff --git a/tests/test_pkcs11.py b/tests/test_pkcs11.py new file mode 100644 index 00000000..cba1a3f0 --- /dev/null +++ b/tests/test_pkcs11.py @@ -0,0 +1,57 @@ +import xmlsec +from tests import base +from xmlsec import constants as consts + +KEY_URL = 'pkcs11;pkcs11:token=test;object=test;pin-value=secret1' + + +def setUpModule(): + from tests import softhsm_setup + + softhsm_setup.setup() + + +def tearDownModule(): + from tests import softhsm_setup + + softhsm_setup.teardown() + + +class TestKeys(base.TestMemoryLeaks): + def test_del_key(self): + ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) + ctx.key = xmlsec.Key.from_engine(KEY_URL) + del ctx.key + self.assertIsNone(ctx.key) + + def test_set_key(self): + ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) + ctx.key = xmlsec.Key.from_engine(KEY_URL) + self.assertIsNotNone(ctx.key) + + def test_sign_bad_args(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_engine(KEY_URL) + with self.assertRaises(TypeError): + ctx.sign('') + + def test_sign_fail(self): + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_engine(KEY_URL) + with self.assertRaisesRegex(xmlsec.Error, 'failed to sign'): + ctx.sign(self.load_xml('sign1-in.xml')) + + def test_sign_case1(self): + """Should sign a pre-constructed template file using a key from a pkcs11 engine.""" + root = self.load_xml('sign1-in.xml') + sign = xmlsec.tree.find_node(root, consts.NodeSignature) + self.assertIsNotNone(sign) + + ctx = xmlsec.SignatureContext() + ctx.key = xmlsec.Key.from_engine(KEY_URL) + self.assertIsNotNone(ctx.key) + ctx.key.name = 'rsakey.pem' + self.assertEqual('rsakey.pem', ctx.key.name) + + ctx.sign(sign) + self.assertEqual(self.load_xml('sign1-out.xml'), root) diff --git a/tests/test_templates.py b/tests/test_templates.py index dcd8d940..bbf7f42d 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -1,86 +1,116 @@ -from tests import base +import unittest -import xmlsec +from lxml import etree +import xmlsec +from tests import base consts = xmlsec.constants class TestTemplates(base.TestMemoryLeaks): def test_create(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create( - root, - c14n_method=consts.TransformExclC14N, - sign_method=consts.TransformRsaSha1, - id="Id", - ns="test" + root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1, id='Id', ns='test' ) - self.assertEqual("Id", sign.get("Id")) - self.assertEqual("test", sign.prefix) + self.assertEqual('Id', sign.get('Id')) + self.assertEqual('test', sign.prefix) + + def test_create_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.create('', c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) def test_encrypt_data_create(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') enc = xmlsec.template.encrypted_data_create( - root, method=consts.TransformDes3Cbc, id="Id", type="Type", mime_type="MimeType", encoding="Encoding", - ns="test" + root, method=consts.TransformDes3Cbc, id='Id', type='Type', mime_type='MimeType', encoding='Encoding', ns='test' ) - for a in ("Id", "Type", "MimeType", "Encoding"): + for a in ('Id', 'Type', 'MimeType', 'Encoding'): self.assertEqual(a, enc.get(a)) - self.assertEqual("test", enc.prefix) + self.assertEqual('test', enc.prefix) def test_ensure_key_info(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) - ki = xmlsec.template.ensure_key_info(sign, id="Id") - self.assertEqual("Id", ki.get("Id")) + ki = xmlsec.template.ensure_key_info(sign, id='Id') + self.assertEqual('Id', ki.get('Id')) + + def test_ensure_key_info_fail(self): + with self.assertRaisesRegex(xmlsec.Error, 'cannot ensure key info.'): + xmlsec.template.ensure_key_info(etree.fromstring(b''), id='Id') + + def test_ensure_key_info_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.ensure_key_info('', id=0) def test_add_encrypted_key(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) ek = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep) - self.assertEqual( - ek, - xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeEncryptedKey, consts.EncNs) - ) - ek2 = xmlsec.template.add_encrypted_key( - ki, consts.TransformRsaOaep, id="Id", type="Type", recipient="Recipient" - ) - for a in ("Id", "Type", "Recipient"): + self.assertEqual(ek, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeEncryptedKey, consts.EncNs)) + ek2 = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep, id='Id', type='Type', recipient='Recipient') + for a in ('Id', 'Type', 'Recipient'): self.assertEqual(a, ek2.get(a)) def test_add_key_name(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) kn = xmlsec.template.add_key_name(ki) - self.assertEqual( - kn, - xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeKeyName, consts.DSigNs) - ) - kn2 = xmlsec.template.add_key_name(ki, name="name") - self.assertEqual("name", kn2.text) + self.assertEqual(kn, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeKeyName, consts.DSigNs)) + kn2 = xmlsec.template.add_key_name(ki, name='name') + self.assertEqual('name', kn2.text) + + def test_add_key_name_none(self): + root = self.load_xml('doc.xml') + sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) + ki = xmlsec.template.ensure_key_info(sign) + kn2 = xmlsec.template.add_key_name(ki, name=None) + self.assertEqual(kn2.text, None) + print(etree.tostring(kn2)) + + def test_add_key_name_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.add_key_name('') def test_add_reference(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) - ref = xmlsec.template.add_reference(sign, consts.TransformSha1, id="Id", uri="URI", type="Type") - for a in ("Id", "URI", "Type"): + ref = xmlsec.template.add_reference(sign, consts.TransformSha1, id='Id', uri='URI', type='Type') + for a in ('Id', 'URI', 'Type'): self.assertEqual(a, ref.get(a)) + def test_add_reference_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.add_reference('', consts.TransformSha1) + with self.assertRaises(TypeError): + xmlsec.template.add_reference(etree.Element('root'), '') + + def test_add_reference_fail(self): + with self.assertRaisesRegex(xmlsec.Error, 'cannot add reference.'): + xmlsec.template.add_reference(etree.Element('root'), consts.TransformSha1) + + def test_add_transform_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.add_transform('', consts.TransformSha1) + with self.assertRaises(TypeError): + xmlsec.template.add_transform(etree.Element('root'), '') + def test_add_key_value(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) kv = xmlsec.template.add_key_value(ki) - self.assertEqual( - kv, - xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeKeyValue, consts.DSigNs) - ) + self.assertEqual(kv, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeKeyValue, consts.DSigNs)) + + def test_add_key_value_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.add_key_value('') def test_add_x509_data(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) x509 = xmlsec.template.add_x509_data(ki) @@ -91,52 +121,93 @@ def test_add_x509_data(self): xmlsec.template.x509_data_add_subject_name(x509) xmlsec.template.x509_issuer_serial_add_issuer_name(issuer) xmlsec.template.x509_issuer_serial_add_serial_number(issuer) - self.assertEqual( - x509, - xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeX509Data, consts.DSigNs) - ) + self.assertEqual(x509, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeX509Data, consts.DSigNs)) + + def test_add_x509_data_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.add_x509_data('') def test_x509_issuer_serial_add_issuer(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) x509 = xmlsec.template.add_x509_data(ki) issuer = xmlsec.template.x509_data_add_issuer_serial(x509) - name = xmlsec.template.x509_issuer_serial_add_issuer_name(issuer, name="Name") - serial = xmlsec.template.x509_issuer_serial_add_serial_number(issuer, serial="Serial") - self.assertEqual("Name", name.text) - self.assertEqual("Serial", serial.text) + name = xmlsec.template.x509_issuer_serial_add_issuer_name(issuer, name='Name') + serial = xmlsec.template.x509_issuer_serial_add_serial_number(issuer, serial='Serial') + self.assertEqual('Name', name.text) + self.assertEqual('Serial', serial.text) + + def test_x509_issuer_serial_add_issuer_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.x509_data_add_issuer_serial('') + + def test_x509_issuer_serial_add_issuer_name_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.x509_issuer_serial_add_issuer_name('') + + def test_x509_issuer_serial_add_serial_number_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.x509_issuer_serial_add_serial_number('') + + def test_x509_data_add_subject_name_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.x509_data_add_subject_name('') + + def test_x509_data_add_ski_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.x509_data_add_ski('') + + def test_x509_data_add_certificate_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.x509_data_add_certificate('') + + def test_x509_data_add_crl_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.x509_data_add_crl('') + + def test_add_encrypted_key_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.add_encrypted_key('', 0) + + def test_encrypted_data_create_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.encrypted_data_create('', 0) def test_encrypted_data_ensure_cipher_value(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') enc = xmlsec.template.encrypted_data_create(root, method=consts.TransformDes3Cbc) cv = xmlsec.template.encrypted_data_ensure_cipher_value(enc) - self.assertEqual( - cv, - xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeCipherValue, consts.EncNs) - ) + self.assertEqual(cv, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeCipherValue, consts.EncNs)) + + def test_encrypted_data_ensure_cipher_value_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.encrypted_data_ensure_cipher_value('') def test_encrypted_data_ensure_key_info(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') enc = xmlsec.template.encrypted_data_create(root, method=consts.TransformDes3Cbc) ki = xmlsec.template.encrypted_data_ensure_key_info(enc) - self.assertEqual( - ki, - xmlsec.tree.find_node(self.load_xml("enc_template.xml"), consts.NodeKeyInfo, consts.DSigNs) - ) - ki2 = xmlsec.template.encrypted_data_ensure_key_info(enc, id="Id", ns="test") - self.assertEqual("Id", ki2.get("Id")) - self.assertEqual("test", ki2.prefix) + self.assertEqual(ki, xmlsec.tree.find_node(self.load_xml('enc_template.xml'), consts.NodeKeyInfo, consts.DSigNs)) + ki2 = xmlsec.template.encrypted_data_ensure_key_info(enc, id='Id', ns='test') + self.assertEqual('Id', ki2.get('Id')) + self.assertEqual('test', ki2.prefix) + + def test_encrypted_data_ensure_key_info_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.encrypted_data_ensure_key_info('') + @unittest.skipIf(not hasattr(consts, 'TransformXslt'), reason='XSLT transformations not enabled') def test_transform_add_c14n_inclusive_namespaces(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ref = xmlsec.template.add_reference(sign, consts.TransformSha1) trans1 = xmlsec.template.add_transform(ref, consts.TransformEnveloped) - xmlsec.template.transform_add_c14n_inclusive_namespaces(trans1, "default") + xmlsec.template.transform_add_c14n_inclusive_namespaces(trans1, 'default') trans2 = xmlsec.template.add_transform(ref, consts.TransformXslt) - xmlsec.template.transform_add_c14n_inclusive_namespaces(trans2, ["ns1", "ns2"]) - self.assertEqual( - ref, - xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeReference, consts.DSigNs) - ) + xmlsec.template.transform_add_c14n_inclusive_namespaces(trans2, ['ns1', 'ns2']) + self.assertEqual(ref, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeReference, consts.DSigNs)) + + def test_transform_add_c14n_inclusive_namespaces_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.template.transform_add_c14n_inclusive_namespaces('', []) diff --git a/tests/test_tree.py b/tests/test_tree.py index f07c654c..5e80a60a 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -1,31 +1,45 @@ -from tests import base - import xmlsec - +from tests import base consts = xmlsec.constants class TestTree(base.TestMemoryLeaks): def test_find_child(self): - root = self.load_xml("sign_template.xml") + root = self.load_xml('sign_template.xml') si = xmlsec.tree.find_child(root, consts.NodeSignedInfo, consts.DSigNs) self.assertEqual(consts.NodeSignedInfo, si.tag.partition('}')[2]) self.assertIsNone(xmlsec.tree.find_child(root, consts.NodeReference)) self.assertIsNone(xmlsec.tree.find_child(root, consts.NodeSignedInfo, consts.EncNs)) + def test_find_child_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.tree.find_child('', 0, True) + def test_find_parent(self): - root = self.load_xml("sign_template.xml") + root = self.load_xml('sign_template.xml') si = xmlsec.tree.find_child(root, consts.NodeSignedInfo, consts.DSigNs) self.assertIs(root, xmlsec.tree.find_parent(si, consts.NodeSignature)) self.assertIsNone(xmlsec.tree.find_parent(root, consts.NodeSignedInfo)) + def test_find_parent_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.tree.find_parent('', 0, True) + def test_find_node(self): - root = self.load_xml("sign_template.xml") + root = self.load_xml('sign_template.xml') ref = xmlsec.tree.find_node(root, consts.NodeReference) self.assertEqual(consts.NodeReference, ref.tag.partition('}')[2]) self.assertIsNone(xmlsec.tree.find_node(root, consts.NodeReference, consts.EncNs)) + def test_find_node_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.tree.find_node('', 0, True) + def test_add_ids(self): - root = self.load_xml("sign_template.xml") - xmlsec.tree.add_ids(root, ["id1", "id2", "id3"]) + root = self.load_xml('sign_template.xml') + xmlsec.tree.add_ids(root, ['id1', 'id2', 'id3']) + + def test_add_ids_bad_args(self): + with self.assertRaises(TypeError): + xmlsec.tree.add_ids('', []) diff --git a/tests/test_type_stubs.py b/tests/test_type_stubs.py index 3b1c3757..82f7df7f 100644 --- a/tests/test_type_stubs.py +++ b/tests/test_type_stubs.py @@ -1,7 +1,6 @@ """Test type stubs for correctness where possible.""" import os -import sys import pytest @@ -10,12 +9,6 @@ black = pytest.importorskip('black') -if sys.version_info >= (3, 4): - from pathlib import Path -else: - from _pytest.pathlib import Path - - constants_stub_header = """ import sys from typing import NamedTuple @@ -25,24 +18,29 @@ else: from typing_extensions import Final - class __KeyData(NamedTuple): # __KeyData type href: str name: str +class __KeyDataNoHref(NamedTuple): # __KeyData type + href: None + name: str class __Transform(NamedTuple): # __Transform type href: str name: str usage: int +class __TransformNoHref(NamedTuple): # __Transform type + href: None + name: str + usage: int """ def gen_constants_stub(): - """ - Generate contents of the file:`xmlsec/constants.pyi`. + """Generate contents of the file:`xmlsec/constants.pyi`. Simply load all constants at runtime, generate appropriate type hint for each constant type. @@ -51,7 +49,10 @@ def gen_constants_stub(): def process_constant(name): """Generate line in stub file for constant name.""" obj = getattr(xmlsec.constants, name) - return '{name}: Final = {obj!r}'.format(name=name, obj=obj) + type_name = type(obj).__name__ + if type_name in ('__KeyData', '__Transform') and obj.href is None: + type_name += 'NoHref' + return f'{name}: Final[{type_name}]' names = list(sorted(name for name in dir(xmlsec.constants) if not name.startswith('__'))) lines = [process_constant(name) for name in names] @@ -59,13 +60,11 @@ def process_constant(name): def test_xmlsec_constants_stub(request): - """ - Generate the stub file for :mod:`xmlsec.constants` from existing code. + """Generate the stub file for :mod:`xmlsec.constants` from existing code. Compare it against the existing stub :file:`xmlsec/constants.pyi`. """ - rootdir = Path(str(request.config.rootdir)) - stub = rootdir / 'src' / 'xmlsec' / 'constants.pyi' - mode = black.FileMode(target_versions=[black.TargetVersion.PY38], line_length=130, is_pyi=True, string_normalization=False) + stub = request.config.rootpath / 'src' / 'xmlsec' / 'constants.pyi' + mode = black.FileMode(target_versions={black.TargetVersion.PY39}, line_length=130, is_pyi=True, string_normalization=False) formatted = black.format_file_contents(gen_constants_stub(), fast=False, mode=mode) assert formatted == stub.read_text() diff --git a/tests/test_xmlsec.py b/tests/test_xmlsec.py new file mode 100644 index 00000000..52dce2b3 --- /dev/null +++ b/tests/test_xmlsec.py @@ -0,0 +1,13 @@ +import xmlsec +from tests import base + + +class TestModule(base.TestMemoryLeaks): + def test_reinitialize_module(self): + """This test doesn't explicitly verify anything, but will be invoked first in the suite. + + So if the subsequent tests don't fail, we know that the ``init()``/``shutdown()`` + function pair doesn't break anything. + """ + xmlsec.shutdown() + xmlsec.init() diff --git a/typeshed/lxml/etree.pyi b/typeshed/lxml/etree.pyi deleted file mode 100644 index 9420180f..00000000 --- a/typeshed/lxml/etree.pyi +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... # incomplete - -class _Element: - def __getattr__(self, name: str) -> Any: ... # incomplete diff --git a/xmlsec_extra.py b/xmlsec_extra.py deleted file mode 100644 index 9bf6c86a..00000000 --- a/xmlsec_extra.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import sys - -try: - from urlparse import urljoin - from urllib import urlretrieve, urlcleanup -except ImportError: - from urllib.parse import urljoin - from urllib.request import urlretrieve, urlcleanup - - -# use pre-built libraries on Windows -def get_prebuilt_libs(download_dir, static_include_dirs, static_library_dirs): - assert sys.platform.startswith('win') - libs = download_and_extract_windows_binaries(download_dir) - for ln, path in libs.items(): - if ln == 'xmlsec1': - i = os.path.join(path, 'include', 'xmlsec1') - else: - i = os.path.join(path, 'include') - - l = os.path.join(path, 'lib') - assert os.path.exists(i), 'does not exist: %s' % i - assert os.path.exists(l), 'does not exist: %s' % l - static_include_dirs.append(i) - static_library_dirs.append(l) - - -def download_and_extract_windows_binaries(destdir): - url = "https://github.com/bgaifullin/libxml2-win-binaries/releases/download/v2018.08/" - if sys.version_info < (3, 5): - if sys.maxsize > 2147483647: - suffix = "vs2008.win64" - else: - suffix = "vs2008.win32" - else: - if sys.maxsize > 2147483647: - suffix = "win64" - else: - suffix = "win32" - - libs = { - 'libxml2': 'libxml2-2.9.4.{}.zip'.format(suffix), - 'libxslt': 'libxslt-1.1.29.{}.zip'.format(suffix), - 'zlib': 'zlib-1.2.8.{}.zip'.format(suffix), - 'iconv': 'iconv-1.14.{}.zip'.format(suffix), - 'openssl': 'openssl-1.0.1.{}.zip'.format(suffix), - 'xmlsec': 'xmlsec-1.2.24.{}.zip'.format(suffix), - } - - if not os.path.exists(destdir): - os.makedirs(destdir) - - for ln, fn in libs.items(): - srcfile = urljoin(url, fn) - destfile = os.path.join(destdir, fn) - if os.path.exists(destfile + ".keep"): - print('Using local copy of "{}"'.format(srcfile)) - else: - print('Retrieving "%s" to "%s"' % (srcfile, destfile)) - urlcleanup() # work around FTP bug 27973 in Py2.7.12+ - urlretrieve(srcfile, destfile) - - libs[ln] = unpack_zipfile(destfile, destdir) - - return libs - - -def find_top_dir_of_zipfile(zipfile): - topdir = None - files = [f.filename for f in zipfile.filelist] - dirs = [d for d in files if d.endswith('/')] - if dirs: - dirs.sort(key=len) - topdir = dirs[0] - topdir = topdir[:topdir.index("/")+1] - for path in files: - if not path.startswith(topdir): - topdir = None - break - assert topdir, ( - "cannot determine single top-level directory in zip file %s" % - zipfile.filename) - return topdir.rstrip('/') - - -def unpack_zipfile(zipfn, destdir): - assert zipfn.endswith('.zip') - import zipfile - print('Unpacking %s into %s' % (os.path.basename(zipfn), destdir)) - f = zipfile.ZipFile(zipfn) - try: - extracted_dir = os.path.join(destdir, find_top_dir_of_zipfile(f)) - f.extractall(path=destdir) - finally: - f.close() - assert os.path.exists(extracted_dir), 'missing: %s' % extracted_dir - return extracted_dir diff --git a/xmlsec_setupinfo.py b/xmlsec_setupinfo.py deleted file mode 100644 index bcfbd321..00000000 --- a/xmlsec_setupinfo.py +++ /dev/null @@ -1,258 +0,0 @@ -from __future__ import print_function - -import glob -import os -import pkg_resources -import sys - -from distutils.errors import DistutilsOptionError - - -WIN32 = sys.platform.lower().startswith('win') - -__MODULE_NAME = "xmlsec" -__MODULE_VERSION = None -__MODULE_DESCRIPTION = "Python bindings for the XML Security Library" -__MODULE_REQUIREMENTS = None -__XMLSEC_CONFIG = None - - -def name(): - return __MODULE_NAME - - -def version(): - global __MODULE_VERSION - if __MODULE_VERSION is None: - with open(os.path.join(get_base_dir(), 'version.txt')) as f: - __MODULE_VERSION = f.read().strip() - return __MODULE_VERSION - - -def description(): - return __MODULE_DESCRIPTION - - -def sources(): - return sorted(glob.glob(os.path.join(get_base_dir(), "src", "*.c"))) - - -def define_macros(): - macros = [ - ("MODULE_NAME", __MODULE_NAME), - ("MODULE_VERSION", version()), - ] - if OPTION_ENABLE_DEBUG: - macros.append(("PYXMLSEC_ENABLE_DEBUG", "1")) - - macros.extend(xmlsec_config()['define_macros']) - - return macros - - -def cflags(): - options = [] - if WIN32: - options.append("/Zi") - else: - options.append("-g") - options.append("-std=c99") - options.append("-fno-strict-aliasing") - options.append("-Wno-error=declaration-after-statement") - options.append("-Werror=implicit-function-declaration") - - if OPTION_ENABLE_DEBUG: - options.append("-Wall") - options.append("-O0") - else: - options.append("-Os") - - return options - - -def include_dirs(): - import lxml - - dirs = xmlsec_config()['include_dirs'] - dirs.extend(lxml.get_include()) - return dirs - - -def libraries(): - return xmlsec_config()['libraries'] - - -def library_dirs(): - return xmlsec_config()['library_dirs'] - - -def dev_status(): - _version = version() - if 'a' in _version: - return 'Development Status :: 3 - Alpha' - elif 'b' in _version or 'c' in _version: - return 'Development Status :: 4 - Beta' - else: - return 'Development Status :: 5 - Production/Stable' - - -def requirements(): - global __MODULE_REQUIREMENTS - if __MODULE_REQUIREMENTS is None: - with open(os.path.join(get_base_dir(), "requirements.txt")) as f: - __MODULE_REQUIREMENTS = [str(req) for req in pkg_resources.parse_requirements(f)] - return __MODULE_REQUIREMENTS - - -def xmlsec_config(): - global __XMLSEC_CONFIG - - if __XMLSEC_CONFIG is None: - __XMLSEC_CONFIG = load_xmlsec1_config() - - return __XMLSEC_CONFIG - - -def load_xmlsec1_config(): - config = None - - if WIN32: - import xmlsec_extra - - config = { - 'define_macros': [ - ('XMLSEC_CRYPTO', '\\"openssl\\"'), - ('__XMLSEC_FUNCTION__', '__FUNCTION__'), - ('XMLSEC_NO_GOST', '1'), - ('XMLSEC_NO_XKMS', '1'), - ('XMLSEC_NO_CRYPTO_DYNAMIC_LOADING', '1'), - ('XMLSEC_CRYPTO_OPENSSL', '1'), - ('UNICODE', '1'), - ('_UNICODE', '1'), - ('LIBXML_ICONV_ENABLED', 1), - ('LIBXML_STATIC', '1'), - ('LIBXSLT_STATIC', '1'), - ('XMLSEC_STATIC', '1'), - ('inline', '__inline'), - ], - 'libraries': [ - 'libxmlsec_a', - 'libxmlsec-openssl_a', - 'libeay32', - 'iconv_a', - 'libxslt_a', - 'libexslt_a', - 'libxml2_a', - 'zlib', - 'WS2_32', - 'Advapi32', - 'User32', - 'Gdi32', - 'Crypt32', - ], - 'include_dirs': [], - 'library_dirs': [], - } - - xmlsec_extra.get_prebuilt_libs( - OPTION_DOWNLOAD_DIR, config['include_dirs'], config['library_dirs'] - ) - else: - import pkgconfig - - try: - config = pkgconfig.parse('xmlsec1') - except EnvironmentError: - pass - - if config is None or not config.get('libraries'): - fatal_xmlsec1_error() - - # make sure that all options are list - for x in ('libraries', 'include_dirs', 'library_dirs'): - config[x] = list(config.get(x) or []) - - # fix macros, ensure that macros is list - macros = list(config.get('define_macros', [])) - for i, v in enumerate(macros): - if v[0] == 'XMLSEC_CRYPTO' and not (v[1].startswith('"') and v[1].endswith('"')): - macros[i] = ('XMLSEC_CRYPTO', '"{0}"'.format(v[1])) - break - config['define_macros'] = macros - return config - - -def fatal_xmlsec1_error(): - print('*********************************************************************************') - print('Could not find xmlsec1 config. Are libxmlsec1-dev and pkg-config installed?') - if sys.platform in ('darwin',): - print('Perhaps try: xcode-select --install') - print('*********************************************************************************') - sys.exit(1) - - -def get_base_dir(): - return os.path.abspath(os.path.dirname(sys.argv[0])) - - -if sys.version_info[0] >= 3: - _system_encoding = sys.getdefaultencoding() - if _system_encoding is None: - _system_encoding = "iso-8859-1" - - def decode_input(data): - if isinstance(data, str): - return data - return data.decode(_system_encoding) -else: - def decode_input(data): - return data - - -def env_var(n): - value = os.getenv(n) - if value: - value = decode_input(value) - if sys.platform == 'win32' and ';' in value: - return value.split(';') - else: - return value.split() - else: - return [] - - -def env_var_name(n): - return "PYXMLSEC_" + n.upper().replace('-', '_') - - -# Option handling: - -def has_option(n): - try: - sys.argv.remove('--%s' % n) - return True - except ValueError: - pass - # allow passing all cmd line options also as environment variables - env_val = os.getenv(env_var_name(n), 'false').lower() - return env_val in ("true", "1") - - -def option_value(n, default=None): - for index, option in enumerate(sys.argv): - if option == '--' + n: - if index+1 >= len(sys.argv): - raise DistutilsOptionError( - 'The option %s requires a value' % option) - value = sys.argv[index+1] - sys.argv[index:index+2] = [] - return value - if option.startswith('--' + n + '='): - value = option[len(n)+3:] - sys.argv[index:index+1] = [] - return value - return os.getenv(env_var_name(n), default) - - -OPTION_ENABLE_DEBUG = has_option('enable-debug') -OPTION_DOWNLOAD_DIR = option_value('download-dir', 'build/extra')