diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index a9a7cde2..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: 1.3.{build} - -environment: - matrix: - - python: 27 - - python: 27-x64 - - python: 35 - - python: 35-x64 - - python: 36 - - python: 36-x64 - - python: 37 - - python: 37-x64 - -install: - - SET PATH=C:\\Python%PYTHON%;c:\\Python%PYTHON%\\scripts;%PATH% - - python -m pip.__main__ install -U pip wheel setuptools - - pip install -r requirements-test.txt - -build: off -build_script: - # configure version - - ps: >- - If ($env:APPVEYOR_REPO_TAG -Eq "true" ) { - $version = "$env:APPVEYOR_REPO_TAG_NAME" - } Else { - $version = "$env:APPVEYOR_BUILD_VERSION.dev0" - } - $version | Set-Content version.txt - - python setup.py build bdist_wheel - - ps: Get-ChildItem dist\*.whl | % { pip install $_.FullName } - -test: off -test_script: - - pip list - - py.test -v tests - - 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 new file mode 100644 index 00000000..e2e2a0df --- /dev/null +++ b/.github/workflows/macosx.yml @@ -0,0 +1,54 @@ +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: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + static_deps: ["static", ""] + steps: + - 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 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 "PKGVER=$(python setup.py --version)" >> $GITHUB_ENV + echo "LLVM_PROFILE_FILE=pyxmlsec.profraw" >> $GITHUB_ENV + - name: Install test dependencies + run: | + 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: | + 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/sdist.yml b/.github/workflows/sdist.yml new file mode 100644 index 00000000..ecc53c31 --- /dev/null +++ b/.github/workflows/sdist.yml @@ -0,0 +1,36 @@ +name: sdist +on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'master' }} +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 + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - 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 + 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: | + pytest -v --color=yes 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 be6e8c15..15f47985 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ !.travis* !.appveyor* !.git* +!.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.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..93665c84 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: ubuntu-20.04 + tools: + python: '3.9' + +sphinx: + configuration: doc/source/conf.py + +python: + install: + - method: pip + path: . + - requirements: doc/source/requirements.txt diff --git a/.travis.yml b/.travis.yml index 398c0b57..8d3ca07e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,44 +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.4 - - python: 3.5 - - python: 3.6 - - python: 3.7 - 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 + addons: apt: packages: - - libssl-dev - - libxmlsec1 - - libxmlsec1-dev - - libxmlsec1-openssl - - libxslt1-dev - - pkg-config -before_install: -- echo "${TRAVIS_TAG:-1.0.1.dev}" >version.txt + - libssl-dev + - libxmlsec1 + - libxmlsec1-dev + - libxmlsec1-openssl + - libxslt1-dev + - pkg-config + - lcov + install: -- travis_retry pip install -r requirements-test.txt -- travis_retry pip install -e "." -- pip list -script: py.test -v tests + - 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 --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 -- python setup.py build_sphinx -deploy: - skip_cleanup: true - skip_upload_docs: false - provider: pypi - user: mehcode - on: - tags: true - distributions: sdist bdist_wheel - python: '2.7' - password: - secure: KNBbGaiAWirptxcuLjqNVgQ9E3jc75kj2+A5MRh+XvDVf6xFLAhrNg0+p/a3d+Tdap4/W5retxDdafLoMJ80ftyJFWV+lnrDxBhcrT686HGXPb0FCn58+Mho0RhM7CSHJIM8E85pVUV5E6QrmgmNWG8KKEaF3Vs5RJBhlP/g7Bg= + - 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/MANIFEST.in b/MANIFEST.in index 9cafd112..6c47dc9c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,14 @@ -include src/* -include requirements*.txt -include LICENSE -include version.txt -include xmlsec_setupinfo.py -include xmlsec_extra.py +recursive-include src * +recursive-include tests * +prune */__pycache__ +prune .github +prune doc +exclude .appveyor.yml +exclude .editorconfig +exclude .travis.yml +exclude .gitattributes +exclude .gitignore +exclude requirements-test.txt +exclude requirements.txt +exclude xmlsec_extra.py +exclude xmlsec_setupinfo.py 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 79a8d97f..00000000 --- a/README.rst +++ /dev/null @@ -1,217 +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/20rtt2wv96gag9cy?svg=true - :target: https://ci.appveyor.com/project/bgaifullin/python-xmlsec -.. image:: https://img.shields.io/pypi/v/xmlsec.svg - :target: https://pypi.python.org/pypi/xmlsec -.. image:: https://img.shields.io/badge/docs-latest-green.svg - :target: http://pythonhosted.org/xmlsec/ - - -Python bindings for the XML Security Library. - -****** -Usage -****** - -Check the `examples `_ to see various examples of signing and verifying using the library. - -************ -Requirements -************ -- libxml2 >= 2.9.1 -- libxmlsec1 >= 1.2.14 - -******* -Install -******* - -Pre-Install ------------ - -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 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 - - -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 - - -Automated ---------- -1. **xmlsec** can be installed through `easy_install` or `pip`. - -.. code-block:: bash - - pip install xmlsec - - -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 (Wheel) -^^^^^^^^^^^^^^^ - -#. Download appropriate binary wheel from `ci.appveyor.com `_ (see build`s artifacts). - -#. Install wheel - - .. code-block:: bash - - pip install - - -Windows (pip) -^^^^^^^^^^^^^ - -#. Configure build environment, see `wiki.python.org `_ for more details. - -#. Install from pip - - .. code-block:: bash - - pip install xmlsec - - -Manual ------- - -#. Clone the **xmlsec** repository to your local computer. - - .. code-block:: bash - - git clone git://github.com/mehcode/python-xmlsec.git - -#. Change into the **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 <#manual>`_. - - -#. 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` which - can be installed by following `link `_ - -#. 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 - - py.test tests - -#. Tests configuration - Env variable **PYXMLSEC_TEST_ITERATIONS** specifies number of test iterations to detect memory leaks. - -Reporting a issue ------------------ -Please attach the output of following information: -version of python-xmlsec -version of libxmlsec1 -version of libxml2 - -output from command: - -.. code-block:: bash - - pkg-config --cflags xmlsec1 - - -****************** -Versions of python -****************** - -The following versions of python is supported: - - - python2.7 - - python3.4 - - python3.5 (required libxmlsec1 >= 1.2.18 and libxml2 >= 2.9.1) - - python3.6 (required libxmlsec1 >= 1.2.18 and libxml2 >= 2.9.1) - - python3.7 (required libxmlsec1 >= 1.2.18 and libxml2 >= 2.9.1) - -******* -License -******* - -Unless otherwise noted, all files contained within this project are liensed under the MIT opensource license. -See the included file LICENSE or visit `opensource.org `_ for more information. diff --git a/build_support/__init__.py b/build_support/__init__.py new file mode 100644 index 00000000..e69de29b 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 0bdbb022..900b79da 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,156 +1,97 @@ -# -*- coding: utf-8 -*- -# -# python-xmlsec documentation build configuration file, created by -# sphinx-quickstart on Fri Mar 17 10:30:14 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.viewcode'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'python-xmlsec' -copyright = u'2017, Bulat Gaifullin ' -author = u'Bulat Gaifullin ' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = os.getenv("TRAVIS_TAG", '1.0.1') -# The full version, including alpha/beta/rc tags. -release = version - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False +from __future__ import annotations +import importlib.metadata +import urllib.request -# -- Options for HTML output ---------------------------------------------- +import lxml +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 -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'nature' +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} +intersphinx_mapping = {'python': ('https://docs.python.org/3/', None)} -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' +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}' -# -- Options for HTMLHelp output ------------------------------------------ +exclude_patterns: list[str] = [] +pygments_style = 'sphinx' +todo_include_todos = False -# Output file base name for HTML help builder. +html_theme = 'furo' +html_static_path: list[str] = [] htmlhelp_basename = 'python-xmlsecdoc' +latex_elements: dict[str, str] = {} +latex_documents = [ + ( + master_doc, + 'python-xmlsec.tex', + 'python-xmlsec Documentation', + 'Bulat Gaifullin \\textless{}gaifullinbf@gmail.com\\textgreater{}', + 'manual', + ) +] -# -- Options for LaTeX output --------------------------------------------- +man_pages = [(master_doc, 'python-xmlsec', 'python-xmlsec Documentation', [author], 1)] -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', +texinfo_documents = [ + ( + master_doc, + 'python-xmlsec', + 'python-xmlsec Documentation', + author, + 'python-xmlsec', + 'One line description of project.', + 'Miscellaneous', + ) +] - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', +autodoc_member_order = 'groupwise' +autodoc_docstring_signature = True - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} +rst_prolog = """ +.. role:: xml(code) + :language: xml +""" -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'python-xmlsec.tex', u'python-xmlsec Documentation', - u'Bulat Gaifullin \\textless{}gaifullinbf@gmail.com\\textgreater{}', 'manual'), -] +# 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' -# -- Options for manual page output --------------------------------------- +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. -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'python-xmlsec', u'python-xmlsec Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- + We handle only :class:`lxml.etree._Element` and :class:`~lxml.etree._Element` nodes. + """ + if ( + node.get('reftype', None) == 'class' + and node.get('reftarget', None) == 'lxml.etree._Element' + and contnode.astext() in ('lxml.etree._Element', '_Element') + ): + 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 -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'python-xmlsec', u'python-xmlsec Documentation', - author, 'python-xmlsec', 'One line description of project.', - 'Miscellaneous'), -] -autodoc_member_order = 'groupwise' +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.') + app.connect('missing-reference', lxml_element_doc_reference) diff --git a/doc/source/examples.rst b/doc/source/examples.rst index 830aaeb9..b5f9bb95 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -3,114 +3,33 @@ Examples Decrypt ------- -.. code:: python - - from lxml import etree - import xmlsec - - manager = xmlsec.KeysManager() - 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) - decrypted = enc_ctx.decrypt(enc_data) - print(etree.tostring(decrypted)) +.. literalinclude:: examples/decrypt.py Encrypt ------- -.. code:: python - - from lxml import etree - 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() - enc_data = xmlsec.template.encrypted_data_create( - template, xmlsec.constants.TransformAes128Cbc, type=xmlsec.constants.TypeEncContent, 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_OAEP) - xmlsec.template.encrypted_data_ensure_cipher_value(enc_key) - data = template.find('./Data') - - # Encryption - enc_ctx = xmlsec.EncryptionContext(manager) - 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) - print(etree.tostring(cipher_value)) +.. literalinclude:: examples/encrypt.py Sign ---- -.. code:: python - - from lxml import etree - import xmlsec - - template = etree.parse('sign1-tmpl.xml').getroot() - - signature_node = xmlsec.tree.find_node(template, xmlsec.constants.NodeSignature) - ctx = xmlsec.SignatureContext() - key = xmlsec.Key.from_file('rsakey.pem', xmlsec.constants.KeyDataFormatPem) - ctx.key = key - ctx.sign(signature_node) - print(etree.tostring(template)) +.. literalinclude:: examples/sign.py Sign-Binary ----------- -.. code:: python - - from lxml import etree - import xmlsec - - ctx = xmlsec.SignatureContext() - key = xmlsec.Key.from_file('rsakey.pem', xmlsec.constants.KeyDataFormatPem) - ctx.key = key - data = b'\xa8f4dP\x82\x02\xd3\xf5.\x02\xc1\x03\xef\xc4\x86\xabC\xec\xb7>\x8e\x1f\xa3\xa3\xc5\xb9qc\xc2\x81\xb1-\xa4B\xdf\x03>\xba\xd1' - sign = ctx.sign_binary(data, xmlsec.constants.TransformRsaSha1) - print(sign) +.. literalinclude:: examples/sign_binary.py Verify ------ -.. code:: python - from lxml import etree - import xmlsec - - template = etree.parse('sign1-res.xml').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() - key = xmlsec.Key.from_file('rsapub.pem', xmlsec.constants.KeyDataFormatPem) - # Set the key on the context. - ctx.key = key - ctx.verify(signature_node) +.. literalinclude:: examples/verify.py Verify-Binary ------------- -.. code:: python - - from lxml import etree - import xmlsec - - ctx = xmlsec.SignatureContext() - key = xmlsec.Key.from_file('rsakey.pem', xmlsec.constants.KeyDataFormatPem) - ctx.key = key - data = b'\xa8f4dP\x82\x02\xd3\xf5.\x02\xc1\x03\xef\xc4\x86\xabC\xec\xb7>\x8e\x1f\xa3\xa3\xc5\xb9qc\xc2\x81\xb1-\xa4B\xdf\x03>\xba\xd1' - sign = b"h\xcb\xb1\x82\xfa`e\x89x\xe5\xc5ir\xd6\xd1Q\x9a\x0b\xeaU_G\xcc'\xa4c\xa3>\x9b27\xbf^`\xa7p\xfb\x98\xcb\x81\xd2\xb1\x0c'\x9d\xe2\n\xec\xb2<\xcf@\x98=\xe0}O8}fy\xc2\xc4\xe9\xec\x87\xf6\xc1\xde\xfd\x96*o\xab\xae\x12\xc9{\xcc\x0e\x93y\x9a\x16\x80o\x92\xeb\x02^h|\xa0\x9b<\x99_\x97\xcb\xe27\xe9u\xc3\xfa_\xcct/sTb\xa0\t\xd3\x93'\xb4\xa4\x0ez\xcbL\x14D\xdb\xe3\x84\x886\xe9J[\xe7\xce\xc0\xb1\x99\x07\x17{\xc6:\xff\x1dt\xfd\xab^2\xf7\x9e\xa4\xccT\x8e~b\xdb\x9a\x04\x04\xbaM\xfa\xbd\xec)z\xbb\x89\xd7\xb2Q\xac\xaf\x13\xdcD\xcd\n6\x92\xfao\xb9\xd9\x96$\xce\xa6\xcf\xf8\xe4Bb60\xf5\xd2a\xb1o\x8c\x0f\x8bl\x88vh\xb5h\xfa\xfa\xb66\xedQ\x10\xc4\xef\xfa\x81\xf0\xc9.^\x98\x1ePQS\x9e\xafAy\x90\xe4\x95\x03V\xc2\xa0\x18\xa5d\xc2\x15*\xb6\xd7$\xc0\t2\xa1" - ctx.verify_binary(data, xmlsec.constants.TransformRsaSha1, sign) +.. literalinclude:: examples/verify_binary.py diff --git a/doc/source/examples/decrypt.py b/doc/source/examples/decrypt.py new file mode 100644 index 00000000..cb474d22 --- /dev/null +++ b/doc/source/examples/decrypt.py @@ -0,0 +1,12 @@ +from lxml import etree + +import xmlsec + +manager = xmlsec.KeysManager() +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) +decrypted = enc_ctx.decrypt(enc_data) +print(etree.tostring(decrypted)) diff --git a/doc/source/examples/enc1-doc.xml b/doc/source/examples/enc1-doc.xml new file mode 100644 index 00000000..39f8d761 --- /dev/null +++ b/doc/source/examples/enc1-doc.xml @@ -0,0 +1,7 @@ + + + +Hello, World! + diff --git a/doc/source/examples/enc1-res.xml b/doc/source/examples/enc1-res.xml new file mode 100644 index 00000000..ab0b1a6c --- /dev/null +++ b/doc/source/examples/enc1-res.xml @@ -0,0 +1,22 @@ + + + + + + + + +UrTgE0UxQa8xevs4SyRA0rsibEz/ZFDjCBD+t4pKSdajB/cefYObZzqq2l41Q6R/ +tqYLht5hEBh26AHfjmQSJAL+eChXOt/EaOf63zzJedO90HGqIQyzOeOPURAl3Li8 +ivPyLVyocJDeVNeh7W+7kYwpFQ6PLuQxWsFFQXVoRAWbXHpZkSzVheR+5RpYJRTb +1UYXKxu8jg4NqbjucVMDIxUOzsVCDRyk8R8sQrM7D/H/N0y7DAY8oX/WZ45xLwUy +DY/U86tTpTn95NwHD10SLyrL6rpXdbEuoIQHhWLwV9uQxnJA/Pn1KZ+xXK/fePfP +26PBo/hUrN5pm5U8ycc4iw== + + + + +2pb5Mxd0f+AW56Cs3MfQ9HJkUVeliSi1hVCNCVHTKeMyC2VL6lPhQ9+L01aSeTSY + + + diff --git a/doc/source/examples/encrypt.py b/doc/source/examples/encrypt.py new file mode 100644 index 00000000..2a92264e --- /dev/null +++ b/doc/source/examples/encrypt.py @@ -0,0 +1,33 @@ +from lxml import etree + +import xmlsec + +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', +) + +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.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_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) +print(etree.tostring(cipher_value)) diff --git a/doc/source/examples/rsacert.pem b/doc/source/examples/rsacert.pem new file mode 100644 index 00000000..e8a68228 --- /dev/null +++ b/doc/source/examples/rsacert.pem @@ -0,0 +1,83 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 5 (0x5) + Signature Algorithm: md5WithRSAEncryption + Issuer: 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 + Validity + Not Before: Mar 31 04:02:22 2003 GMT + Not After : Mar 28 04:02:22 2013 GMT + Subject: C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Examples RSA Certificate, CN=Aleksey Sanin/emailAddress=xmlsec@aleksey.com + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (2048 bit) + Modulus (2048 bit): + 00:97:b8:fe:b4:3f:83:35:78:16:89:04:ec:2b:61: + 8c:bf:c4:5f:00:81:4a:45:e6:d9:cd:e9:e2:3c:97: + 3b:45:ad:aa:e6:8d:0b:77:71:07:01:4f:7c:f9:7d: + e2:19:aa:dd:91:59:f4:f1:cf:3d:ba:78:46:96:11: + 9c:b6:5b:46:39:73:55:23:aa:f7:9e:00:5c:e5:e9: + 49:ec:3b:9c:3f:84:99:3a:90:ad:df:7e:64:86:c6: + 26:72:ce:31:08:79:7e:13:15:b8:e5:bf:d6:56:02: + 8d:60:21:4c:27:18:64:fb:fb:55:70:f6:33:bd:2f: + 55:70:d5:5e:7e:99:ae:a4:e0:aa:45:47:13:a8:30: + d5:a0:8a:9d:cc:20:ec:e4:8e:51:c9:54:c5:7f:3e: + 66:2d:74:bf:a3:7a:f8:f3:ec:94:57:39:b4:ac:00: + 75:62:61:54:b4:d0:e0:52:86:f8:5e:77:ec:50:43: + 9c:d2:ba:a7:8c:62:5a:bc:b2:fe:f3:cc:62:7e:23: + 60:6b:c7:51:49:37:78:7e:25:15:30:ab:fa:b4:ae: + 25:8f:22:fc:a3:48:7f:f2:0a:8a:6e:e0:fe:8d:f0: + 01:ed:c6:33:cc:6b:a1:fd:a6:80:ef:06:8c:af:f6: + 40:3a:8e:42:14:20:61:12:1f:e3:fc:05:b1:05:d5: + 65:c3 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + 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: + 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 + + Signature Algorithm: md5WithRSAEncryption + b5:3f:9b:32:31:4a:ff:2f:84:3b:a8:9b:11:5c:a6:5c:f0:76: + 52:d9:6e:f4:90:ad:fa:0d:90:c1:98:d5:4a:12:dd:82:6b:37: + e8:d9:2d:62:92:c9:61:37:98:86:8f:a4:49:6a:5e:25:d0:18: + 69:30:0f:98:8f:43:58:89:31:b2:3b:05:e2:ef:c7:a6:71:5f: + f7:fe:73:c5:a7:b2:cd:2e:73:53:71:7d:a8:4c:68:1a:32:1b: + 5e:48:2f:8f:9b:7a:a3:b5:f3:67:e8:b1:a2:89:4e:b2:4d:1b: + 79:9c:ff:f0:0d:19:4f:4e:b1:03:3d:99:f0:44:b7:8a:0b:34: + 9d:83 +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/doc/source/examples/rsakey.pem b/doc/source/examples/rsakey.pem new file mode 100644 index 00000000..55d2fd9b --- /dev/null +++ b/doc/source/examples/rsakey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAl7j+tD+DNXgWiQTsK2GMv8RfAIFKRebZzeniPJc7Ra2q5o0L +d3EHAU98+X3iGardkVn08c89unhGlhGctltGOXNVI6r3ngBc5elJ7DucP4SZOpCt +335khsYmcs4xCHl+ExW45b/WVgKNYCFMJxhk+/tVcPYzvS9VcNVefpmupOCqRUcT +qDDVoIqdzCDs5I5RyVTFfz5mLXS/o3r48+yUVzm0rAB1YmFUtNDgUob4XnfsUEOc +0rqnjGJavLL+88xifiNga8dRSTd4fiUVMKv6tK4ljyL8o0h/8gqKbuD+jfAB7cYz +zGuh/aaA7waMr/ZAOo5CFCBhEh/j/AWxBdVlwwIDAQABAoIBAQCAvt6DnZF9gdW9 +l4vAlBqXb88d4phgELCp5tmviLUnP2NSGEWuqR7Eoeru2z9NgIxblvYfazh6Ty22 +kmNk6rcAcTnB9oYAcVZjUj8EUuEXlTFhXPvuNpafNu3RZd59znqJP1mSu+LpQWku +NZMlabHnkTLDlGf7FXtvL9/rlgV4qk3QcDVF793JFszWrtK3mnld3KHQ6cuo9iSm +0rQKtkDjeHsRell8qTQvfBsgG1q2bv8QWT45/eQrra9mMbGTr3DbnXvoeJmTj1VN +XJV7tBNllxxPahlYMByJaf/Tuva5j6HWUEIfYky5ihr2z1P/fNQ2OSCM6SQHpkiG +EXQDueXBAoGBAMfW7KcmToEQEcTiqfey6C1LOLoemcX0/ROUktPq/5JQJRRrT4t7 +XevLX0ed8sLyR5T29XQtdnuV0DJfvcJD+6ZwfOcQ+f6ZzCaNXJP97JtEt5kSWY01 +Ei+nphZ0RFvPb04V3qDU9dElU26GR36CRBYJyM2WQPx4v+/YyDSZH9kLAoGBAMJc +ZBU8pRbIia/FFOHUlS3v5P18nVmXyOd0fvRq0ZelaQCebTZ4K9wjnCfw//yzkb2Z +0vZFNB+xVBKB0Pt6nVvnSNzxdQ8EAXVFwHtXa25FUyP2RERQgTvmajqmgWjZsDYp +6GHcK3ZhmdmscQHF/Q2Uo4scvBcheahm9IXiNskpAoGAXelEgTBhSAmTMCEMmti6 +fz6QQ/bJcNu2apMxhOE0hT+gjT34vaWV9481EWTKho5w0TJVGumaem1mz6VqeXaV +Nhw6tiOmN91ysNNRpEJ6BGWAmjCjYNaF21s/k+HDlhmfRuTEIHSzqDuQP6pewrbY +5Dpo4SQxGfRsznvjacRj0Q0CgYBN247oBvQnDUxCkhNMZ8kersOvW5T4x9neBge5 +R3UQZ12Jtu0O7dK8C7PJODyDcTeHmTAuIQjBTVrdUw1xP+v7XcoNX9hBnJws6zUw +85MAiFrGxCcSqqEqaqHRPtQGOXXiLKV/ViA++tgTn4VhbXtyTkG5P1iFd45xjFSV +sUm7CQKBgDn92tHxzePly1L1mK584TkVryx4cP9RFHpebnmNduGwwjnRuYipoj8y +pPPAkVbbaA3f9OB2go48rN0Ft9nHdlqgh9BpIKCVtkIb1XN0K3Oa/8BW8W/GAiNG +HJcsrOtIrGVRdlyJG6bDaN8T49DnhOcsqMbf+IkIvfh50VeE9L/e +-----END RSA PRIVATE KEY----- diff --git a/doc/source/examples/rsapub.pem b/doc/source/examples/rsapub.pem new file mode 100644 index 00000000..838a346d --- /dev/null +++ b/doc/source/examples/rsapub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl7j+tD+DNXgWiQTsK2GM +v8RfAIFKRebZzeniPJc7Ra2q5o0Ld3EHAU98+X3iGardkVn08c89unhGlhGctltG +OXNVI6r3ngBc5elJ7DucP4SZOpCt335khsYmcs4xCHl+ExW45b/WVgKNYCFMJxhk ++/tVcPYzvS9VcNVefpmupOCqRUcTqDDVoIqdzCDs5I5RyVTFfz5mLXS/o3r48+yU +Vzm0rAB1YmFUtNDgUob4XnfsUEOc0rqnjGJavLL+88xifiNga8dRSTd4fiUVMKv6 +tK4ljyL8o0h/8gqKbuD+jfAB7cYzzGuh/aaA7waMr/ZAOo5CFCBhEh/j/AWxBdVl +wwIDAQAB +-----END PUBLIC KEY----- diff --git a/doc/source/examples/sign.py b/doc/source/examples/sign.py new file mode 100644 index 00000000..519c13a0 --- /dev/null +++ b/doc/source/examples/sign.py @@ -0,0 +1,13 @@ +from lxml import etree + +import xmlsec + +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() +key = xmlsec.Key.from_file('rsakey.pem', xmlsec.constants.KeyDataFormatPem) +ctx.key = key +ctx.sign(signature_node) +print(etree.tostring(template)) diff --git a/doc/source/examples/sign1-res.xml b/doc/source/examples/sign1-res.xml new file mode 100644 index 00000000..f46ac1f4 --- /dev/null +++ b/doc/source/examples/sign1-res.xml @@ -0,0 +1,31 @@ + + + + + Hello, World! + + + + + + + + + + + 9H/rQr2Axe9hYTV2n/tCp+3UIQQ= + + + Mx4psIy9/UY+u8QBJRDrwQWKRaCGz0WOVftyDzAe6WHAFSjMNr7qb2ojq9kdipT8 +Oub5q2OQ7mzdSLiiejkrO1VeqM/90yEIGI4En6KEB6ArEzw+iq4N1wm6EptcyxXx +M9StAOOa9ilWYqR9Tfx3SW1urUIuKYgUitxsONiUHBVaW6HeX51bsXoTF++4ZI+D +jiPBjN4HHmr0cbJ6BXk91S27ffZIfp1Qj5nL9onFLUGbR6EFgu2luiRzQbPuM2tP +XxyI7GZ8AfHnRJK28ARvBC9oi+O1ej20S79CIV7gdBxbLbFprozBHAwOEC57YgJc +x+YEjSjcO7SBIR1FiUA7pw== + + rsakey.pem + + + diff --git a/doc/source/examples/sign1-tmpl.xml b/doc/source/examples/sign1-tmpl.xml new file mode 100644 index 00000000..0a0cd442 --- /dev/null +++ b/doc/source/examples/sign1-tmpl.xml @@ -0,0 +1,26 @@ + + + + + Hello, World! + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/examples/sign_binary.py b/doc/source/examples/sign_binary.py new file mode 100644 index 00000000..275c6e40 --- /dev/null +++ b/doc/source/examples/sign_binary.py @@ -0,0 +1,8 @@ +import xmlsec + +ctx = xmlsec.SignatureContext() +key = xmlsec.Key.from_file('rsakey.pem', xmlsec.constants.KeyDataFormatPem) +ctx.key = key +data = b'\xa8f4dP\x82\x02\xd3\xf5.\x02\xc1\x03\xef\xc4\x86\xabC\xec\xb7>\x8e\x1f\xa3\xa3\xc5\xb9qc\xc2\x81\xb1-\xa4B\xdf\x03>\xba\xd1' +sign = ctx.sign_binary(data, xmlsec.constants.TransformRsaSha1) +print(sign) diff --git a/doc/source/examples/verify.py b/doc/source/examples/verify.py new file mode 100644 index 00000000..c3240c99 --- /dev/null +++ b/doc/source/examples/verify.py @@ -0,0 +1,15 @@ +from lxml import etree + +import xmlsec + +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() +key = xmlsec.Key.from_file('rsapub.pem', xmlsec.constants.KeyDataFormatPem) +# Set the key on the context. +ctx.key = key +ctx.verify(signature_node) diff --git a/doc/source/examples/verify_binary.py b/doc/source/examples/verify_binary.py new file mode 100644 index 00000000..1f888b99 --- /dev/null +++ b/doc/source/examples/verify_binary.py @@ -0,0 +1,9 @@ +import xmlsec + +ctx = xmlsec.SignatureContext() +key = xmlsec.Key.from_file('rsakey.pem', xmlsec.constants.KeyDataFormatPem) +ctx.key = key + +data = b'\xa8f4dP\x82\x02\xd3\xf5.\x02\xc1\x03\xef\xc4\x86\xabC\xec\xb7>\x8e\x1f\xa3\xa3\xc5\xb9qc\xc2\x81\xb1-\xa4B\xdf\x03>\xba\xd1' +sign = b"h\xcb\xb1\x82\xfa`e\x89x\xe5\xc5ir\xd6\xd1Q\x9a\x0b\xeaU_G\xcc'\xa4c\xa3>\x9b27\xbf^`\xa7p\xfb\x98\xcb\x81\xd2\xb1\x0c'\x9d\xe2\n\xec\xb2<\xcf@\x98=\xe0}O8}fy\xc2\xc4\xe9\xec\x87\xf6\xc1\xde\xfd\x96*o\xab\xae\x12\xc9{\xcc\x0e\x93y\x9a\x16\x80o\x92\xeb\x02^h|\xa0\x9b<\x99_\x97\xcb\xe27\xe9u\xc3\xfa_\xcct/sTb\xa0\t\xd3\x93'\xb4\xa4\x0ez\xcbL\x14D\xdb\xe3\x84\x886\xe9J[\xe7\xce\xc0\xb1\x99\x07\x17{\xc6:\xff\x1dt\xfd\xab^2\xf7\x9e\xa4\xccT\x8e~b\xdb\x9a\x04\x04\xbaM\xfa\xbd\xec)z\xbb\x89\xd7\xb2Q\xac\xaf\x13\xdcD\xcd\n6\x92\xfao\xb9\xd9\x96$\xce\xa6\xcf\xf8\xe4Bb60\xf5\xd2a\xb1o\x8c\x0f\x8bl\x88vh\xb5h\xfa\xfa\xb66\xedQ\x10\xc4\xef\xfa\x81\xf0\xc9.^\x98\x1ePQS\x9e\xafAy\x90\xe4\x95\x03V\xc2\xa0\x18\xa5d\xc2\x15*\xb6\xd7$\xc0\t2\xa1" +ctx.verify_binary(data, xmlsec.constants.TransformRsaSha1, sign) diff --git a/doc/source/index.rst b/doc/source/index.rst index 6c3d313f..e08f47d9 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,7 +1,7 @@ .. python-xmlsec documentation master file, created by sphinx-quickstart on Fri Mar 17 10:30:14 2017. You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. + contain the root ``toctree`` directive. Welcome to python-xmlsec's documentation! ========================================= diff --git a/doc/source/install.rst b/doc/source/install.rst index 57107c75..c892a3ea 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -1,67 +1,89 @@ -Install ------------ +Installation +============ -Linux (Debian) -^^^^^^^^^^^^^^ +``xmlsec`` is available on PyPI: .. code-block:: bash - apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl - pip install xmlsec + pip install xmlsec +Depending on your OS, you may need to install the required native +libraries first: -Note: There is no required version of libxml2 for ubuntu precise, -so need to download and install it manually. +Linux (Debian) +-------------- .. 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 + 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 - pip install xmlsec + 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 - xcode-select --install - brew upgrade - brew install libxml2 libxmlsec1 pkg-config - pip install xmlsec + xcode-select --install + brew upgrade + brew install libxml2 libxmlsec1 pkg-config +Alpine +------ -Windows (Wheel) -^^^^^^^^^^^^^^^ +.. code-block:: bash -#. Download appropriate binary wheels from `appveyor `_ (see build`s artifacts). + apk add build-base openssl libffi-dev openssl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec -#. Install downloaded wheel - .. code-block:: bash +Troubleshooting +*************** - pip install +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 (pip) -^^^^^^^^^^^^^ -#. Configure build environment, see `wiki.python.org `_ for more details. +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: -#. Install from pip +#. Configure build environment, see `wiki.python.org `_ for more details. - .. code-block:: bash +#. Install from source dist: - pip install xmlsec + .. code-block:: bash + pip install xmlsec --no-binary=xmlsec diff --git a/doc/source/modules/constants.rst b/doc/source/modules/constants.rst index 67f2d711..3df6b50f 100644 --- a/doc/source/modules/constants.rst +++ b/doc/source/modules/constants.rst @@ -1,154 +1,495 @@ -xmlsec.constants ----------------- +``xmlsec.constants`` +-------------------- -Various constants used by the library +Various constants used by the library. EncryptionType ************** -- *TypeEncContent* - http://www.w3.org/2001/04/xmlenc#Content -- *TypeEncElement* - http://www.w3.org/2001/04/xmlenc#Element + +.. data:: xmlsec.constants.TypeEncContent + :annotation: = 'http://www.w3.org/2001/04/xmlenc#Content' + +.. data:: xmlsec.constants.TypeEncElement + :annotation: = 'http://www.w3.org/2001/04/xmlenc#Element' KeyData ******* -- *KeyDataName* - The processing class. -- *KeyDataValue* - The processing class. -- *KeyDataRetrievalMethod* - The processing class. -- *KeyDataEncryptedKey* - The processing class. -- *KeyDataAes* - The AES key klass. -- *KeyDataDes* - The DES key klass. -- *KeyDataDsa* - The DSA key klass. -- *KeyDataEcdsa* - The ECDSA key klass. -- *KeyDataHmac* - The DHMAC key klass. -- *KeyDataRsa* - The RSA key klass. -- *KeyDataX509* - The X509 data klass. -- *KeyDataRawX509Cert* - The raw X509 certificate klass. + +.. class:: __KeyData + + Base type for all :samp:`KeyData{XXX}` constants. + +.. data:: xmlsec.constants.KeyDataName + + The :xml:`` processing class. + +.. data:: xmlsec.constants.KeyDataValue + + The :xml:`` processing class. + +.. data:: xmlsec.constants.KeyDataRetrievalMethod + + The :xml:`` processing class. + +.. data:: xmlsec.constants.KeyDataEncryptedKey + + The :xml:`` processing class. + +.. data:: xmlsec.constants.KeyDataAes + + The AES key klass. + +.. data:: xmlsec.constants.KeyDataDes + + The DES key klass. + +.. data:: xmlsec.constants.KeyDataDsa + + The DSA key klass. + +.. data:: xmlsec.constants.KeyDataEcdsa + + (Deprecated. The EC key klass) The ECDSA key klass. + +.. data:: xmlsec.constants.KeyDataEc + + The EC key klass. + +.. data:: xmlsec.constants.KeyDataHmac + + The DHMAC key klass. + +.. data:: xmlsec.constants.KeyDataRsa + + The RSA key klass. + +.. data:: xmlsec.constants.KeyDataX509 + + The X509 data klass. + +.. data:: xmlsec.constants.KeyDataRawX509Cert + + The raw X509 certificate klass. KeyDataFormat ************* -- *KeyDataFormatUnknown* - the key data format is unknown. -- *KeyDataFormatBinary* - the binary key data. -- *KeyDataFormatPem* - the PEM key data (cert or public/private key). -- *KeyDataFormatDer* - the DER key data (cert or public/private key). -- *KeyDataFormatPkcs8Pem* - the PKCS8 PEM private key. -- *KeyDataFormatPkcs8Der* - the PKCS8 DER private key. -- *KeyDataFormatPkcs12* - the PKCS12 format (bag of keys and certs) -- *KeyDataFormatCertPem* - the PEM cert. -- *KeyDataFormatCertDer* - the DER cert. + +.. data:: xmlsec.constants.KeyDataFormatUnknown + + the key data format is unknown. + +.. data:: xmlsec.constants.KeyDataFormatBinary + + the binary key data. + +.. data:: xmlsec.constants.KeyDataFormatPem + + the PEM key data (cert or public/private key). + +.. data:: xmlsec.constants.KeyDataFormatDer + + the DER key data (cert or public/private key). + +.. data:: xmlsec.constants.KeyDataFormatPkcs8Pem + + the PKCS8 PEM private key. + +.. data:: xmlsec.constants.KeyDataFormatPkcs8Der + + the PKCS8 DER private key. + +.. data:: xmlsec.constants.KeyDataFormatPkcs12 + + the PKCS12 format (bag of keys and certs) + +.. data:: xmlsec.constants.KeyDataFormatCertPem + + the PEM cert. + +.. data:: xmlsec.constants.KeyDataFormatCertDer + + the DER cert. KeyDataType *********** -- *KeyDataTypeUnknown* - The key data type is unknown -- *KeyDataTypeNone* - The key data type is unknown -- *KeyDataTypePublic* - The key data contain a public key. -- *KeyDataTypePrivate* - The key data contain a private key. -- *KeyDataTypeSymmetric* - The key data contain a symmetric key. -- *KeyDataTypeSession* - The key data contain session key (one time key, not stored in keys manager). -- *KeyDataTypePermanent* - The key data contain permanent key (stored in keys manager). -- *KeyDataTypeTrusted* - The key data is trusted. -- *KeyDataTypeAny* - The key data is trusted. + +.. data:: xmlsec.constants.KeyDataTypeUnknown + + The key data type is unknown + +.. data:: xmlsec.constants.KeyDataTypeNone + + The key data type is unknown + +.. data:: xmlsec.constants.KeyDataTypePublic + + The key data contain a public key. + +.. data:: xmlsec.constants.KeyDataTypePrivate + + The key data contain a private key. + +.. data:: xmlsec.constants.KeyDataTypeSymmetric + + The key data contain a symmetric key. + +.. data:: xmlsec.constants.KeyDataTypeSession + + The key data contain session key (one time key, not stored in keys manager). + +.. data:: xmlsec.constants.KeyDataTypePermanent + + The key data contain permanent key (stored in keys manager). + +.. data:: xmlsec.constants.KeyDataTypeTrusted + + The key data is trusted. + +.. data:: xmlsec.constants.KeyDataTypeAny + + The key data is trusted. Namespaces ********** -- *Ns* - http://www.aleksey.com/xmlsec/2002 -- *DSigNs* - http://www.w3.org/2000/09/xmldsig# -- *EncNs* - http://www.w3.org/2001/04/xmlenc# -- *XPathNs* - http://www.w3.org/TR/1999/REC-xpath-19991116 -- *XPath2Ns* - http://www.w3.org/2002/06/xmldsig-filter2 -- *XPointerNs* - http://www.w3.org/2001/04/xmldsig-more/xptr -- *Soap11Ns* - http://schemas.xmlsoap.org/soap/envelope/ -- *Soap12Ns* - http://www.w3.org/2002/06/soap-envelope -- *NsExcC14N* - http://www.w3.org/2001/10/xml-exc-c14n# -- *NsExcC14NWithComments* - http://www.w3.org/2001/10/xml-exc-c14n#WithComments +.. data:: xmlsec.constants.Ns + :annotation: = 'http://www.aleksey.com/xmlsec/2002' + +.. data:: xmlsec.constants.DSigNs + :annotation: = 'http://www.w3.org/2000/09/xmldsig#' + +.. data:: xmlsec.constants.EncNs + :annotation: = 'http://www.w3.org/2001/04/xmlenc#' + +.. data:: xmlsec.constants.XPathNs + :annotation: = 'http://www.w3.org/TR/1999/REC-xpath-19991116' + +.. data:: xmlsec.constants.XPath2Ns + :annotation: = 'http://www.w3.org/2002/06/xmldsig-filter2' + +.. data:: xmlsec.constants.XPointerNs + :annotation: = 'http://www.w3.org/2001/04/xmldsig-more/xptr' + +.. data:: xmlsec.constants.NsExcC14N + :annotation: = 'http://www.w3.org/2001/10/xml-exc-c14n#' + +.. data:: xmlsec.constants.NsExcC14NWithComments + :annotation: = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments' Nodes ***** -- *NodeSignature* - Signature -- *NodeSignedInfo* - SignedInfo -- *NodeCanonicalizationMethod* - CanonicalizationMethod -- *NodeSignatureMethod* - SignatureMethod -- *NodeSignatureValue* - SignatureValue -- *NodeSignatureProperties* - SignatureProperties -- *NodeDigestMethod* - DigestMethod -- *NodeDigestValue* - DigestValue -- *NodeObject* - Object -- *NodeManifest* - Manifest -- *NodeEncryptedData* - EncryptedData -- *NodeEncryptedKey* - EncryptedKey -- *NodeEncryptionMethod* - EncryptionMethod -- *NodeEncryptionProperties* - EncryptionProperties -- *NodeEncryptionProperty* - EncryptionProperty -- *NodeCipherData* - CipherData -- *NodeCipherValue* - CipherValue -- *NodeCipherReference* - CipherReference -- *NodeReference - Reference -- *NodeReferenceList* - ReferenceList -- *NodeDataReference* - DataReference -- *NodeKeyReference* - KeyReference -- *NodeKeyInfo* - KeyInfo -- *NodeKeyName - KeyName -- *NodeKeyValue - KeyValue -- *NodeX509Data - X509Data + +.. data:: xmlsec.constants.NodeSignature + :annotation: = 'Signature' + +.. data:: xmlsec.constants.NodeSignedInfo + :annotation: = 'SignedInfo' + +.. data:: xmlsec.constants.NodeCanonicalizationMethod + :annotation: = 'CanonicalizationMethod' + +.. data:: xmlsec.constants.NodeSignatureMethod + :annotation: = 'SignatureMethod' + +.. data:: xmlsec.constants.NodeSignatureValue + :annotation: = 'SignatureValue' + +.. data:: xmlsec.constants.NodeSignatureProperties + :annotation: = 'SignatureProperties' + +.. data:: xmlsec.constants.NodeDigestMethod + :annotation: = 'DigestMethod' + +.. data:: xmlsec.constants.NodeDigestValue + :annotation: = 'DigestValue' + +.. data:: xmlsec.constants.NodeObject + :annotation: = 'Object' + +.. data:: xmlsec.constants.NodeManifest + :annotation: = 'Manifest' + +.. data:: xmlsec.constants.NodeEncryptedData + :annotation: = 'EncryptedData' + +.. data:: xmlsec.constants.NodeEncryptedKey + :annotation: = 'EncryptedKey' + +.. data:: xmlsec.constants.NodeEncryptionMethod + :annotation: = 'EncryptionMethod' + +.. data:: xmlsec.constants.NodeEncryptionProperties + :annotation: = 'EncryptionProperties' + +.. data:: xmlsec.constants.NodeEncryptionProperty + :annotation: = 'EncryptionProperty' + +.. data:: xmlsec.constants.NodeCipherData + :annotation: = 'CipherData' + +.. data:: xmlsec.constants.NodeCipherValue + :annotation: = 'CipherValue' + +.. data:: xmlsec.constants.NodeCipherReference + :annotation: = 'CipherReference' + +.. data:: xmlsec.constants.NodeReference + :annotation: = 'Reference' + +.. data:: xmlsec.constants.NodeReferenceList + :annotation: = 'ReferenceList' + +.. data:: xmlsec.constants.NodeDataReference + :annotation: = 'DataReference' + +.. data:: xmlsec.constants.NodeKeyReference + :annotation: = 'KeyReference' + +.. data:: xmlsec.constants.NodeKeyInfo + :annotation: = 'KeyInfo' + +.. data:: xmlsec.constants.NodeKeyName + :annotation: = 'KeyName' + +.. data:: xmlsec.constants.NodeKeyValue + :annotation: = 'KeyValue' + +.. data:: xmlsec.constants.NodeX509Data + :annotation: = 'X509Data' Transforms ********** -- *TransformUsageUnknown* - Transforms usage is unknown or undefined. -- *TransformUsageDSigTransform* - Transform could be used in . -- *TransformUsageC14NMethod* - Transform could be used in . -- *TransformUsageDigestMethod* - Transform could be used in . -- *TransformUsageSignatureMethod* - Transform could be used in . -- *TransformUsageEncryptionMethod* - Transform could be used in . -- *TransformUsageAny* - Transform could be used for operation. -- *TransformInclC14N* - The regular (inclusive) C14N without comments transform klass. -- *TransformInclC14NWithComments* - The regular (inclusive) C14N with comments transform klass. -- *TransformInclC14N11* - The regular (inclusive) C14N 1.1 without comments transform klass. -- *TransformInclC14N11WithComments* - The regular (inclusive) C14N 1.1 with comments transform klass. -- *TransformExclC14N* - The exclusive C14N without comments transform klass. -- *TransformExclC14NWithComments* - The exclusive C14N with comments transform klass. -- *TransformEnveloped* - The "enveloped" transform klass. -- *TransformXPath* - The XPath transform klass. -- *TransformXPath2* - The XPath2 transform klass. -- *TransformXPointer* - The XPointer transform klass. -- *TransformXslt* - The XSLT transform klass. -- *TransformRemoveXmlTagsC14N* - The "remove all xml tags" transform klass (used before base64 transforms). -- *TransformVisa3DHack* - Selects node subtree by given node id string. The only reason why we need this is Visa3D protocol. It doesn't follow XML/XPointer/XMLDSig specs and allows invalid XPointer expressions in the URI attribute. Since we couldn't evaluate such expressions thru XPath/XPointer engine, we need to have this hack here. -- *TransformAes128Cbc* - The AES128 CBC cipher transform klass. -- *TransformAes192Cbc* - The AES192 CBC cipher transform klass. -- *TransformAes256Cbc* - The AES256 CBC cipher transform klass. -- *TransformKWAes128* - The AES 128 key wrap transform klass. -- *TransformKWAes192* - The AES 192 key wrap transform klass. -- *TransformKWAes256* - The AES 256 key wrap transform klass. -- *TransformDes3Cbc* - The DES3 CBC cipher transform klass. -- *TransformKWDes3* - The DES3 key wrap transform klass. -- *TransformDsaSha1* - The DSA-SHA1 signature transform klass. -- *TransformEcdsaSha1* - The ECDSA-SHA1 signature transform klass. -- *TransformEcdsaSha224* - The ECDSA-SHA224 signature transform klass. -- *TransformEcdsaSha256* - The ECDSA-SHA256 signature transform klass. -- *TransformEcdsaSha384* - The ECDS-SHA384 signature transform klass. -- *TransformEcdsaSha512* - The ECDSA-SHA512 signature transform klass. -- *TransformHmacMd5* - The HMAC with MD5 signature transform klass. -- *TransformHmacRipemd160* - The HMAC with RipeMD160 signature transform klass. -- *TransformHmacSha1* - The HMAC with SHA1 signature transform klass. -- *TransformHmacSha224* - The HMAC with SHA224 signature transform klass. -- *TransformHmacSha256* - The HMAC with SHA256 signature transform klass. -- *TransformHmacSha384* - The HMAC with SHA384 signature transform klass. -- *TransformHmacSha512* - The HMAC with SHA512 signature transform klass. -- *TransformRsaMd5* - The RSA-MD5 signature transform klass. -- *TransformRsaRipemd160* - The RSA-RIPEMD160 signature transform klass. -- *TransformRsaSha1* - The RSA-SHA1 signature transform klass. -- *TransformRsaSha224* - The RSA-SHA224 signature transform klass. -- *TransformRsaSha256* - The RSA-SHA256 signature transform klass. -- *TransformRsaSha384* - The RSA-SHA384 signature transform klass. -- *TransformRsaSha512* - The RSA-SHA512 signature transform klass. -- *TransformRsaPkcs1* - The RSA PKCS1 key transport transform klass. -- *TransformRsaOaep* - The RSA OAEP key transport transform klass. -- *TransformMd5* - The MD5 digest transform klass. -- *TransformRipemd160* - The RIPEMD160 digest transform klass. -- *TransformSha1* - The SHA1 digest transform klass. -- *TransformSha224* - The SHA224 digest transform klass. -- *TransformSha256* - The SHA256 digest transform klass. -- *TransformSha384* - The SHA384 digest transform klass. -- *TransformSha512* - The SHA512 digest transform klass. +.. class:: __Transform + + Base type for all :samp:`Transform{XXX}` constants. + +.. data:: xmlsec.constants.TransformUsageUnknown + + Transforms usage is unknown or undefined. + +.. data:: xmlsec.constants.TransformUsageDSigTransform + + Transform could be used in :xml:``. + +.. data:: xmlsec.constants.TransformUsageC14NMethod + + Transform could be used in :xml:``. + +.. data:: xmlsec.constants.TransformUsageDigestMethod + + Transform could be used in :xml:``. + +.. data:: xmlsec.constants.TransformUsageSignatureMethod + + Transform could be used in :xml:``. + +.. data:: xmlsec.constants.TransformUsageEncryptionMethod + + Transform could be used in :xml:``. + +.. data:: xmlsec.constants.TransformUsageAny + + Transform could be used for operation. + +.. data:: xmlsec.constants.TransformInclC14N + + The regular (inclusive) C14N without comments transform klass. + +.. data:: xmlsec.constants.TransformInclC14NWithComments + + The regular (inclusive) C14N with comments transform klass. + +.. data:: xmlsec.constants.TransformInclC14N11 + + The regular (inclusive) C14N 1.1 without comments transform klass. + +.. data:: xmlsec.constants.TransformInclC14N11WithComments + + The regular (inclusive) C14N 1.1 with comments transform klass. + +.. data:: xmlsec.constants.TransformExclC14N + + The exclusive C14N without comments transform klass. + +.. data:: xmlsec.constants.TransformExclC14NWithComments + + The exclusive C14N with comments transform klass. + +.. data:: xmlsec.constants.TransformEnveloped + + The "enveloped" transform klass. + +.. data:: xmlsec.constants.TransformXPath + + The XPath transform klass. + +.. data:: xmlsec.constants.TransformXPath2 + + The XPath2 transform klass. + +.. data:: xmlsec.constants.TransformXPointer + + The XPointer transform klass. + +.. data:: xmlsec.constants.TransformXslt + + The XSLT transform klass. + +.. data:: xmlsec.constants.TransformRemoveXmlTagsC14N + + The "remove all xml tags" transform klass (used before base64 transforms). + +.. data:: xmlsec.constants.TransformVisa3DHack + + Selects node subtree by given node id string. The only reason why we need this is Visa3D protocol. It doesn't follow XML/XPointer/XMLDSig specs and allows invalid XPointer expressions in the URI attribute. Since we couldn't evaluate such expressions thru XPath/XPointer engine, we need to have this hack here. + +.. data:: xmlsec.constants.TransformAes128Cbc + + The AES128 CBC cipher transform klass. + +.. data:: xmlsec.constants.TransformAes192Cbc + + The AES192 CBC cipher transform klass. + +.. data:: xmlsec.constants.TransformAes256Cbc + + The AES256 CBC cipher transform klass. + +.. data:: xmlsec.constants.TransformKWAes128 + + The AES 128 key wrap transform klass. + +.. data:: xmlsec.constants.TransformKWAes192 + + The AES 192 key wrap transform klass. + +.. data:: xmlsec.constants.TransformKWAes256 + + The AES 256 key wrap transform klass. + +.. data:: xmlsec.constants.TransformDes3Cbc + + The DES3 CBC cipher transform klass. + +.. data:: xmlsec.constants.TransformKWDes3 + + The DES3 key wrap transform klass. + +.. data:: xmlsec.constants.TransformDsaSha1 + + The DSA-SHA1 signature transform klass. + +.. data:: xmlsec.constants.TransformEcdsaSha1 + + The ECDSA-SHA1 signature transform klass. + +.. data:: xmlsec.constants.TransformEcdsaSha224 + + The ECDSA-SHA224 signature transform klass. + +.. data:: xmlsec.constants.TransformEcdsaSha256 + + The ECDSA-SHA256 signature transform klass. + +.. data:: xmlsec.constants.TransformEcdsaSha384 + + The ECDS-SHA384 signature transform klass. + +.. data:: xmlsec.constants.TransformEcdsaSha512 + + The ECDSA-SHA512 signature transform klass. + +.. data:: xmlsec.constants.TransformHmacMd5 + + The HMAC with MD5 signature transform klass. + +.. data:: xmlsec.constants.TransformHmacRipemd160 + + The HMAC with RipeMD160 signature transform klass. + +.. data:: xmlsec.constants.TransformHmacSha1 + + The HMAC with SHA1 signature transform klass. + +.. data:: xmlsec.constants.TransformHmacSha224 + + The HMAC with SHA224 signature transform klass. + +.. data:: xmlsec.constants.TransformHmacSha256 + + The HMAC with SHA256 signature transform klass. + +.. data:: xmlsec.constants.TransformHmacSha384 + + The HMAC with SHA384 signature transform klass. + +.. data:: xmlsec.constants.TransformHmacSha512 + + The HMAC with SHA512 signature transform klass. + +.. data:: xmlsec.constants.TransformRsaMd5 + + The RSA-MD5 signature transform klass. + +.. data:: xmlsec.constants.TransformRsaRipemd160 + + The RSA-RIPEMD160 signature transform klass. + +.. data:: xmlsec.constants.TransformRsaSha1 + + The RSA-SHA1 signature transform klass. + +.. data:: xmlsec.constants.TransformRsaSha224 + + The RSA-SHA224 signature transform klass. + +.. data:: xmlsec.constants.TransformRsaSha256 + + The RSA-SHA256 signature transform klass. + +.. data:: xmlsec.constants.TransformRsaSha384 + + The RSA-SHA384 signature transform klass. + +.. data:: xmlsec.constants.TransformRsaSha512 + + The RSA-SHA512 signature transform klass. + +.. data:: xmlsec.constants.TransformRsaPkcs1 + + The RSA PKCS1 key transport transform klass. + +.. data:: xmlsec.constants.TransformRsaOaep + + The RSA OAEP key transport transform klass. + +.. data:: xmlsec.constants.TransformMd5 + + The MD5 digest transform klass. + +.. data:: xmlsec.constants.TransformRipemd160 + + The RIPEMD160 digest transform klass. + +.. data:: xmlsec.constants.TransformSha1 + + The SHA1 digest transform klass. + +.. data:: xmlsec.constants.TransformSha224 + + The SHA224 digest transform klass. + +.. data:: xmlsec.constants.TransformSha256 + + The SHA256 digest transform klass. + +.. data:: xmlsec.constants.TransformSha384 + + The SHA384 digest transform klass. + +.. data:: xmlsec.constants.TransformSha512 + + The SHA512 digest transform klass. :ref:`contents` diff --git a/doc/source/modules/template.rst b/doc/source/modules/template.rst index c1e4b762..85551d2a 100644 --- a/doc/source/modules/template.rst +++ b/doc/source/modules/template.rst @@ -1,5 +1,5 @@ -xmlsec.template ---------------- +``xmlsec.template`` +------------------- .. automodule:: xmlsec.template :members: diff --git a/doc/source/modules/tree.rst b/doc/source/modules/tree.rst index 0dd70ea4..e70d6509 100644 --- a/doc/source/modules/tree.rst +++ b/doc/source/modules/tree.rst @@ -1,5 +1,5 @@ -xmlsec.tree ------------ +``xmlsec.tree`` +--------------- .. automodule:: xmlsec.tree :members: diff --git a/doc/source/modules/xmlsec.rst b/doc/source/modules/xmlsec.rst index 35a16339..426bbaa4 100644 --- a/doc/source/modules/xmlsec.rst +++ b/doc/source/modules/xmlsec.rst @@ -1,5 +1,5 @@ -xmlsec ------- +``xmlsec`` +---------- .. automodule:: xmlsec :members: diff --git a/doc/source/requirements.txt b/doc/source/requirements.txt new file mode 100644 index 00000000..ffb2b6d3 --- /dev/null +++ b/doc/source/requirements.txt @@ -0,0 +1,5 @@ +lxml==6.0.2 +importlib_metadata;python_version < '3.8' +packaging +Sphinx>=3 +furo>=2021.4.11b34 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..7c7b4bf3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,131 @@ +[build-system] +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 26b77f68..ad135d97 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,2 +1,5 @@ -r requirements.txt -pytest + +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 3b305822..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,5 +12,11 @@ source-dir = doc/source build-dir = doc/build all_files = 1 -[upload_docs] -upload-dir = doc/build/html +# [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 08c56670..4100a52b 100644 --- a/setup.py +++ b/setup.py @@ -1,63 +1,61 @@ -from setuptools import setup -from setuptools import Extension -from setuptools.command import build_ext +from pathlib import Path -import xmlsec_setupinfo +from setuptools import Extension, setup +from build_support.build_ext import build_ext -class BuildExt(build_ext.build_ext): - def run(self): - # at this moment all setup_requires are installed and we can safety import them - self.patch_options() - build_ext.build_ext.run(self) +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>=1.5.1', 'lxml>=3.8'] - def patch_options(self): - ext = self.ext_map[xmlsec_setupinfo.name()] - ext.define_macros.extend(xmlsec_setupinfo.define_macros()) - ext.include_dirs.extend(xmlsec_setupinfo.include_dirs()) - ext.libraries.extend(xmlsec_setupinfo.libraries()) - ext.library_dirs.extend(xmlsec_setupinfo.library_dirs()) +with open('README.md', encoding='utf-8') as readme: + long_desc = readme.read() -_xmlsec = Extension( - xmlsec_setupinfo.name(), - sources=xmlsec_setupinfo.sources(), - extra_compile_args=xmlsec_setupinfo.cflags(), - libraries=[], - library_dirs=[], - include_dirs=[], - define_macros=[], -) setup( - name=xmlsec_setupinfo.name(), - version=xmlsec_setupinfo.version(), - description=xmlsec_setupinfo.description(), - ext_modules=[_xmlsec], - cmdclass={'build_ext': BuildExt}, - setup_requires=xmlsec_setupinfo.requirements(), - install_requires=xmlsec_setupinfo.requirements(), - author="Bulat Gaifullin", + name='xmlsec', + 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='>=3.9', + setup_requires=setup_reqs, + install_requires=['lxml>=3.8'], + author='Bulat Gaifullin', author_email='support@mehcode.com', - maintainer='Bulat Gaifullin', - maintainer_email='gaifullinbf@gmail.com', + maintainer='Oleg Hoefling', + maintainer_email='oleg.hoefling@gmail.com', url='https://github.com/mehcode/python-xmlsec', - download_url="https://github.com/mehcode/python-xmlsec/archive/v%s.tar.gz" % xmlsec_setupinfo.version(), + 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"], + keywords=['xmlsec'], classifiers=[ - xmlsec_setupinfo.dev_status(), + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', '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.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Text Processing :: Markup :: XML' + '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', ], + zip_safe=False, + packages=['xmlsec'], + package_dir={'': 'src'}, + package_data={'xmlsec': ['py.typed', '*.pyi']}, ) 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 a23dfb78..bd1fa5e0 100644 --- a/src/constants.c +++ b/src/constants.c @@ -22,18 +22,35 @@ static void PyXmlSec_Transform__del__(PyObject* self) { static PyObject* PyXmlSec_Transform__str__(PyObject* self) { char buf[300]; PyXmlSec_Transform* transform = (PyXmlSec_Transform*)(self); - snprintf(buf, sizeof(buf), "%s, %s", transform->id->name, transform->id->href); - return PyString_FromString(buf); + if (transform->id->href != NULL) + snprintf(buf, sizeof(buf), "%s, %s", transform->id->name, transform->id->href); + else + snprintf(buf, sizeof(buf), "%s, None", transform->id->name); + + return PyUnicode_FromString(buf); +} + +// __repr__ method +static PyObject* PyXmlSec_Transform__repr__(PyObject* self) { + char buf[300]; + PyXmlSec_Transform* transform = (PyXmlSec_Transform*)(self); + if (transform->id->href != NULL) + 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 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) { - return PyString_FromString((const char*)self->id->href); + if (self->id->href != NULL) + return PyUnicode_FromString((const char*)self->id->href); + Py_RETURN_NONE; } static const char PyXmlSec_TransformUsageGet__doc__[] = "The allowed transforms usages."; @@ -68,44 +85,44 @@ static PyGetSetDef PyXmlSec_TransformGetSet[] = { static PyTypeObject _PyXmlSec_TransformType = { PyVarObject_HEAD_INIT(NULL, 0) - STRINGIFY(MODULE_NAME) "constants.__Transform", /* tp_name */ - sizeof(PyXmlSec_Transform), /* tp_basicsize */ - 0, /* tp_itemsize */ - PyXmlSec_Transform__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 */ - PyXmlSec_Transform__str__, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ - "The xmlSecTransformId reflection", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - 0, /* tp_methods */ - 0, /* tp_members */ - PyXmlSec_TransformGetSet, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ - 0, /* tp_free */ + STRINGIFY(MODULE_NAME) ".constants.__Transform", /* tp_name */ + sizeof(PyXmlSec_Transform), /* tp_basicsize */ + 0, /* tp_itemsize */ + PyXmlSec_Transform__del__, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved */ + PyXmlSec_Transform__repr__, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + PyXmlSec_Transform__str__, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "The xmlSecTransformId reflection", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + PyXmlSec_TransformGetSet, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ + 0, /* tp_free */ }; PyTypeObject* PyXmlSec_TransformType = &_PyXmlSec_TransformType; @@ -128,18 +145,34 @@ static void PyXmlSec_KeyData__del__(PyObject* self) { static PyObject* PyXmlSec_KeyData__str__(PyObject* self) { char buf[300]; PyXmlSec_KeyData* keydata = (PyXmlSec_KeyData*)(self); - snprintf(buf, sizeof(buf), "%s, %s", keydata->id->name, keydata->id->href); - return PyString_FromString(buf); + if (keydata->id->href != NULL) + snprintf(buf, sizeof(buf), "%s, %s", keydata->id->name, keydata->id->href); + else + snprintf(buf, sizeof(buf), "%s, None", keydata->id->name); + return PyUnicode_FromString(buf); +} + +// __repr__ method +static PyObject* PyXmlSec_KeyData__repr__(PyObject* self) { + char buf[300]; + PyXmlSec_KeyData* keydata = (PyXmlSec_KeyData*)(self); + if (keydata->id->href != NULL) + 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 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) { - return PyString_FromString((const char*)self->id->href); + if (self->id->href != NULL) + return PyUnicode_FromString((const char*)self->id->href); + Py_RETURN_NONE; } static PyGetSetDef PyXmlSec_KeyDataGetSet[] = { @@ -162,7 +195,7 @@ static PyGetSetDef PyXmlSec_KeyDataGetSet[] = { static PyTypeObject _PyXmlSec_KeyDataType = { PyVarObject_HEAD_INIT(NULL, 0) - STRINGIFY(MODULE_NAME) "constants.__KeyData", /* tp_name */ + STRINGIFY(MODULE_NAME) ".constants.__KeyData", /* tp_name */ sizeof(PyXmlSec_KeyData), /* tp_basicsize */ 0, /* tp_itemsize */ PyXmlSec_KeyData__del__, /* tp_dealloc */ @@ -170,7 +203,7 @@ static PyTypeObject _PyXmlSec_KeyDataType = { 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ - 0, /* tp_repr */ + PyXmlSec_KeyData__repr__, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ @@ -212,7 +245,6 @@ static PyObject* PyXmlSec_KeyDataNew(xmlSecKeyDataId id) { return (PyObject*)keydata; } -#ifdef PY3K static PyModuleDef PyXmlSec_ConstantsModule = { PyModuleDef_HEAD_INIT, @@ -220,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) { @@ -234,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; @@ -259,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 @@ -275,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 @@ -290,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"); @@ -301,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 @@ -316,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 @@ -394,7 +418,7 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypePublic, "PUBLIC"); PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypePrivate, "PRIVATE"); PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypeSymmetric, "SYMMETRIC"); - PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypeSession, "SESSION");; + PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypeSession, "SESSION"); PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypePermanent, "PERMANENT"); PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypeTrusted, "TRUSTED"); PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypeAny, "ANY"); @@ -415,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") @@ -451,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"); @@ -463,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"); @@ -477,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 @@ -487,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 @@ -499,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 @@ -510,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 5986f566..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; @@ -110,10 +120,14 @@ static int PyXmlSec_SignatureContextKeySet(PyObject* self, PyObject* value, void } static const char PyXmlSec_SignatureContextRegisterId__doc__[] = \ + "register_id(node, id_attr = 'ID', id_ns = None) -> None\n" "Registers new id.\n\n" ":param node: the pointer to XML node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param id_attr: the attribute\n" - ":param id_ns: the namespace\n"; + ":type id_attr: :class:`str`\n" + ":param id_ns: the namespace (optional)\n" + ":type id_ns: :class:`str` or :data:`None`"; static PyObject* PyXmlSec_SignatureContextRegisterId(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "node", "id_attr", "id_ns", NULL}; @@ -166,8 +180,10 @@ static PyObject* PyXmlSec_SignatureContextRegisterId(PyObject* self, PyObject* a } static const char PyXmlSec_SignatureContextSign__doc__[] = \ + "sign(node) -> None\n" "Signs according to the signature template.\n\n" - ":param node: the pointer to node with signature template\n"; + ":param node: the pointer to :xml:`` node with signature template\n" + ":type node: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_SignatureContextSign(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "node", NULL}; @@ -197,9 +213,12 @@ static PyObject* PyXmlSec_SignatureContextSign(PyObject* self, PyObject* args, P } static const char PyXmlSec_SignatureContextVerify__doc__[] = \ + "verify(node) -> None\n" "Verifies according to the signature template.\n\n" - ":param node: he pointer with node\n" - ":return: None if success otherwise raises VerificationError\n"; + ":param node: the pointer with :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: :data:`None` on success\n" + ":raise VerificationError: on failure\n"; static PyObject* PyXmlSec_SignatureContextVerify(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "node", NULL}; @@ -243,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) { @@ -287,10 +307,14 @@ static int PyXmlSec_ProcessSignBinary(PyXmlSec_SignatureContext* ctx, const xmlS } static const char PyXmlSec_SignatureContextSignBinary__doc__[] = \ - "Signs binary data *data* with *algorithm*.\n\n" + "sign_binary(bytes, transform) -> bytes\n" + "Signs binary data ``data`` with algorithm ``transform``.\n\n" ":param bytes: the binary data\n" + ":type bytes: :class:`bytes`\n" ":param transform: the signature algorithm\n" - ":return: the signature\n"; + ":type transform: :class:`__Transform`\n" + ":return: the signature\n" + ":rtype: :class:`bytes`"; static PyObject* PyXmlSec_SignatureContextSignBinary(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "bytes", "transform", NULL}; PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; @@ -322,11 +346,16 @@ static PyObject* PyXmlSec_SignatureContextSignBinary(PyObject* self, PyObject* a } static const char PyXmlSec_SignatureContextVerifyBinary__doc__[] = \ + "verify_binary(bytes, transform, signature) -> None\n" "Verifies signature for binary data.\n\n" ":param bytes: the binary data\n" + ":type bytes: :class:`bytes`\n" ":param transform: the signature algorithm\n" + ":type transform: :class:`__Transform`\n" ":param signature: the signature\n" - ":return: None if success otherwise raises VerificationError\n"; + ":type signature: :class:`bytes`\n" + ":return: :data:`None` on success\n" + ":raise VerificationError: on failure"; static PyObject* PyXmlSec_SignatureContextVerifyBinary(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "bytes", "transform", "signature", NULL}; @@ -372,10 +401,12 @@ static PyObject* PyXmlSec_SignatureContextVerifyBinary(PyObject* self, PyObject* } static const char PyXmlSec_SignatureContextEnableReferenceTransform__doc__[] = \ - "Enables use of *t* as reference transform.\n\n"\ - "Note: by default, all transforms are enabled. The first call of\n"\ - "`enable_reference_transform` will switch to explicitly enabled transforms.\n\n" - ":param transform: the transform klass.\n"; + "enable_reference_transform(transform) -> None\n" + "Enables use of ``transform`` as reference transform.\n\n" + ".. note:: by default, all transforms are enabled. The first call of " + ":meth:`~SignatureContext.enable_reference_transform` will switch to explicitly enabled transforms.\n\n" + ":param transform: the transform klass.\n" + ":type transform: :class:`__Transform`"; static PyObject* PyXmlSec_SignatureContextEnableReferenceTransform(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "transform", NULL}; @@ -406,10 +437,12 @@ static PyObject* PyXmlSec_SignatureContextEnableReferenceTransform(PyObject* sel } static const char PyXmlSec_SignatureContextEnableSignatureTransform__doc__[] = \ - "Enables use of *t* as signature transform.\n\n"\ - "Note: by default, all transforms are enabled. The first call of\n"\ - "`enable_signature_transform` will switch to explicitly enabled transforms.\n\n" - ":param transform: the transform klass.\n"; + "enable_signature_transform(transform) -> None\n" + "Enables use of ``transform`` as signature transform.\n\n" + ".. note:: by default, all transforms are enabled. The first call of " + ":meth:`~SignatureContext.enable_signature_transform` will switch to explicitly enabled transforms.\n\n" + ":param transform: the transform klass.\n" + ":type transform: :class:`__Transform`\n"; static PyObject* PyXmlSec_SignatureContextEnableSignatureTransform(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "transform", NULL}; @@ -439,8 +472,10 @@ static PyObject* PyXmlSec_SignatureContextEnableSignatureTransform(PyObject* sel } static const char PyXmlSec_SignatureContextSetEnabledKeyData__doc__[] = \ - "Adds selected *KeyData* to the list of enabled key data list.\n\n" - ":param keydata_list: the list.\n"; + "set_enabled_key_data(keydata_list) -> None\n" + "Adds selected :class:`__KeyData` to the list of enabled key data list.\n\n" + ":param keydata_list: the list\n" + ":type keydata_list: :class:`list` of :class:`__KeyData`"; static PyObject* PyXmlSec_SignatureContextSetEnabledKeyData(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "keydata_list", NULL}; diff --git a/src/enc.c b/src/enc.c index 9db0b1b8..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; @@ -114,7 +135,8 @@ static int PyXmlSec_EncryptionContextKeySet(PyObject* self, PyObject* value, voi } static const char PyXmlSec_EncryptionContextReset__doc__[] = \ - "Resets *context*, user settings are not touched.\n"; + "reset() -> None\n"\ + "Reset this context, user settings are not touched.\n"; static PyObject* PyXmlSec_EncryptionContextReset(PyObject* self, PyObject* args, PyObject* kwargs) { PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; @@ -128,11 +150,15 @@ static PyObject* PyXmlSec_EncryptionContextReset(PyObject* self, PyObject* args, } static const char PyXmlSec_EncryptionContextEncryptBinary__doc__[] = \ - "Encrypts binary *data* according to `EncryptedData` template *template*\n"\ - "Note: *template* is modified in place.\n\n" - ":param template: the pointer to template node\n" + "encrypt_binary(template, data) -> lxml.etree._Element\n" + "Encrypts binary ``data`` according to ``EncryptedData`` template ``template``.\n\n" + ".. note:: ``template`` is modified in place.\n\n" + ":param template: the pointer to :xml:`` template node\n" + ":type template: :class:`lxml.etree._Element`\n" ":param data: the data\n" - ":return: the resulting subtree\n"; + ":type data: :class:`bytes`\n" + ":return: the resulting :xml:`` subtree\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_EncryptionContextEncryptBinary(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "template", "data", NULL}; @@ -169,6 +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; // release the replaced nodes in a way safe for `lxml` xmlNodePtr n = ctx->replacedNodeList; xmlNodePtr nn; @@ -177,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 - PyXmlSec_LxmlElementPtr* elem = PyXmlSec_elementFactory(doc, n); + elem = (PyXmlSec_LxmlElementPtr)PyXmlSec_elementFactory(doc, n); if (NULL == elem) xmlFreeNode(n); else @@ -188,14 +215,18 @@ static void PyXmlSec_ClearReplacedNodes(xmlSecEncCtxPtr ctx, PyXmlSec_LxmlDocume } static const char PyXmlSec_EncryptionContextEncryptXml__doc__[] = \ - "Encrpyts *node* using *template*.\n" \ - "Note: The `Type` attribute of *template* decides whether *node* itself is encrypted\n"\ - "(`http://www.w3.org/2001/04/xmlenc#Element`) or its content (`http://www.w3.org/2001/04/xmlenc#Content`).\n"\ - "It must have one of these two values (or an exception is raised).\n"\ - "The operation modifies the tree and removes replaced nodes.\n"\ - ":param template: the pointer to template node\n"\ - ":param node: the pointer to node for encryption\n"\ - ":return: the pointer to newly created node\n"; + "encrypt_xml(template, node) -> lxml.etree._Element\n" + "Encrypts ``node`` using ``template``.\n\n" + ".. note:: The ``\"Type\"`` attribute of ``template`` decides whether ``node`` itself " + "(``http://www.w3.org/2001/04/xmlenc#Element``) or its content (``http://www.w3.org/2001/04/xmlenc#Content``) is encrypted.\n" + " It must have one of these two values (or an exception is raised).\n" + " The operation modifies the tree and removes replaced nodes.\n\n" + ":param template: the pointer to :xml:`` template node\n\n" + ":type template: :class:`lxml.etree._Element`\n" + ":param node: the pointer to node for encryption\n\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: the pointer to newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_EncryptionContextEncryptXml(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "template", "node", NULL}; @@ -214,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; } @@ -270,10 +301,15 @@ static PyObject* PyXmlSec_EncryptionContextEncryptXml(PyObject* self, PyObject* } static const char PyXmlSec_EncryptionContextEncryptUri__doc__[] = \ - "Encrypts binary data obtained from *uri* according to *template*.\n\n" - ":param template: the pointer to template node\n" + "encrypt_uri(template, uri) -> lxml.etree._Element\n" + "Encrypts binary data obtained from ``uri`` according to ``template``.\n\n" + ".. note:: ``template`` is modified in place.\n\n" + ":param template: the pointer to :xml:`` template node\n" + ":type template: :class:`lxml.etree._Element`\n" ":param uri: the URI\n" - ":return: the resulting subtree\n"; + ":type uri: :class:`str`\n" + ":return: the resulting :xml:`` subtree\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_EncryptionContextEncryptUri(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "template", "uri", NULL}; @@ -305,14 +341,16 @@ static PyObject* PyXmlSec_EncryptionContextEncryptUri(PyObject* self, PyObject* } static const char PyXmlSec_EncryptionContextDecrypt__doc__[] = \ - "Decrypts *node* (an `EncryptedData` or `EncryptedKey` element) and return the result.\n"\ - "The decryption may result in binary data or an XML subtree.\n"\ - "In the former case, the binary data is returned. In the latter case,\n"\ - "the input tree is modified and a reference to the decrypted XML subtree is returned.\n"\ - "If the operation modifies the tree, it removes replaced nodes.\n"\ - ":param node: the pointer to or node\n" - ":return: depends on input parameters\n"; - + "decrypt(node)\n" + "Decrypts ``node`` (an ``EncryptedData`` or ``EncryptedKey`` element) and returns the result. " + "The decryption may result in binary data or an XML subtree. " + "In the former case, the binary data is returned. In the latter case, " + "the input tree is modified and a reference to the decrypted XML subtree is returned.\n" + "If the operation modifies the tree, it removes replaced nodes.\n\n" + ":param node: the pointer to :xml:`` or :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: depends on input parameters\n" + ":rtype: :class:`lxml.etree._Element` or :class:`bytes`"; static PyObject* PyXmlSec_EncryptionContextDecrypt(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "node", NULL}; 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 01d7fa30..5ff04aae 100644 --- a/src/keys.c +++ b/src/keys.c @@ -68,11 +68,16 @@ static PyObject* PyXmlSec_Key__copy__(PyObject* self) { } static const char PyXmlSec_KeyFromMemory__doc__[] = \ + "from_memory(data, format, password = None) -> xmlsec.Key\n" "Loads PKI key from memory.\n\n" ":param data: the binary key data\n" + ":type data: :class:`str` or :class:`bytes`\n" ":param format: the key file format\n" - ":param password: the key file password\n" - ":return: pointer to newly created key\n"; + ":type format: :class:`int`\n" + ":param password: the key file password (optional)\n" + ":type password: :class:`str` or :data:`None`\n" + ":return: pointer to newly created key\n" + ":rtype: :class:`~xmlsec.Key`"; static PyObject* PyXmlSec_KeyFromMemory(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "data", "format", "password", NULL}; @@ -112,11 +117,17 @@ static PyObject* PyXmlSec_KeyFromMemory(PyObject* self, PyObject* args, PyObject } static const char PyXmlSec_KeyFromFile__doc__[] = \ + "from_file(file, format, password = None) -> xmlsec.Key\n" "Loads PKI key from a file.\n\n" ":param file: the file object or file path\n" + ":type file: :class:`str`, :class:`bytes`, any :class:`~os.PathLike`, " + ":class:`~typing.BinaryIO` or :class:`~typing.TextIO`\n" ":param format: the key file format\n" - ":param password: the key file password\n" - ":return: pointer to newly created key\n"; + ":type format: :class:`int`\n" + ":param password: the key file password (optional)\n" + ":type password: :class:`str` or :data:`None`\n" + ":return: pointer to newly created key\n" + ":rtype: :class:`~xmlsec.Key`"; static PyObject* PyXmlSec_KeyFromFile(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "file", "format", "password", NULL}; @@ -131,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; } @@ -152,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; @@ -174,12 +190,62 @@ 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__[] = \ - "Generates key of kind *data* with *size* and *type*.\n\n" + "generate(klass, size, type) -> xmlsec.Key\n" + "Generates key of kind ``klass`` with ``size`` and ``type``.\n\n" ":param klass: the requested key klass (rsa, dsa, aes, ...)\n" + ":type klass: :class:`__KeyData`\n" ":param size: the new key size (in bits!)\n" + ":type size: :class:`int`\n" ":param type: the new key type (session, permanent, ...)\n" - ":return: pointer to newly created key\n"; + ":type type: :class:`int`\n" + ":return: pointer to newly created key\n" + ":rtype: :class:`~xmlsec.Key`"; static PyObject* PyXmlSec_KeyGenerate(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "klass", "size", "type", NULL}; @@ -214,10 +280,14 @@ static PyObject* PyXmlSec_KeyGenerate(PyObject* self, PyObject* args, PyObject* } static const char PyXmlSec_KeyFromBinaryFile__doc__[] = \ - "Loads (symmetric) key of kind *data* from *filename*.\n\n" + "from_binary_file(klass, filename) -> xmlsec.Key\n" + "Loads (symmetric) key of kind ``klass`` from ``filename``.\n\n" ":param klass: the key value data klass\n" + ":type klass: :class:`__KeyData`\n" ":param filename: the key binary filename\n" - ":return: pointer to newly created key\n"; + ":type filename: :class:`str`, :class:`bytes` or any :class:`~os.PathLike`\n" + ":return: pointer to newly created key\n" + ":rtype: :class:`~xmlsec.Key`"; static PyObject* PyXmlSec_KeyFromBinaryFile(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "klass", "filename", NULL}; @@ -229,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; } @@ -261,10 +331,14 @@ static PyObject* PyXmlSec_KeyFromBinaryFile(PyObject* self, PyObject* args, PyOb } static const char PyXmlSec_KeyFromBinaryData__doc__[] = \ - "Loads (symmetric) key of kind *klass* from *data*.\n\n" + "from_binary_data(klass, data) -> xmlsec.Key\n" + "Loads (symmetric) key of kind ``klass`` from ``data``.\n\n" ":param klass: the key value data klass\n" + ":type klass: :class:`__KeyData`\n" ":param data: the key binary data\n" - ":return: pointer to newly created key\n"; + ":type data: :class:`str` or :class:`bytes`\n" + ":return: pointer to newly created key\n" + ":rtype: :class:`~xmlsec.Key`"; static PyObject* PyXmlSec_KeyFromBinaryData(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "klass", "data", NULL}; @@ -304,9 +378,12 @@ static PyObject* PyXmlSec_KeyFromBinaryData(PyObject* self, PyObject* args, PyOb } static const char PyXmlSec_KeyCertFromMemory__doc__[] = \ + "load_cert_from_memory(data, format) -> None\n" "Loads certificate from memory.\n\n" ":param data: the certificate binary data\n" - ":param format: the certificate file format\n"; + ":type data: :class:`str` or :class:`bytes`\n" + ":param format: the certificate file format\n" + ":type format: :class:`int`"; static PyObject* PyXmlSec_KeyCertFromMemory(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "data", "format", NULL}; @@ -340,9 +417,13 @@ static PyObject* PyXmlSec_KeyCertFromMemory(PyObject* self, PyObject* args, PyOb } static const char PyXmlSec_KeyCertFromFile__doc__[] = \ + "load_cert_from_file(file, format) -> None\n" "Loads certificate from file.\n\n" ":param file: the file object or file path\n" - ":param format: the certificate file format\n"; + ":type file: :class:`str`, :class:`bytes`, any :class:`~os.PathLike`, " + ":class:`~typing.BinaryIO` or :class:`~typing.TextIO`\n" + ":param format: the certificate file format\n" + ":type format: :class:`int`"; static PyObject* PyXmlSec_KeyCertFromFile(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "file", "format", NULL}; @@ -393,7 +474,7 @@ static PyObject* PyXmlSec_KeyCertFromFile(PyObject* self, PyObject* args, PyObje return NULL; } -static const char PyXmlSec_KeyName__doc__[] = "the name of *key*.\n"; +static const char PyXmlSec_KeyName__doc__[] = "the name of this key.\n"; static PyObject* PyXmlSec_KeyNameGet(PyObject* self, void* closure) { PyXmlSec_Key* key = (PyXmlSec_Key*)self; const char* cname; @@ -405,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; } @@ -421,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; } @@ -452,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, @@ -562,12 +660,12 @@ static int PyXmlSec_KeysManager__init__(PyObject* self, PyObject* args, PyObject PYXMLSEC_DEBUGF("%p: init key manager", self); if (handle == NULL) { - PyXmlSec_SetLastError("failed to create xmlsecKeyManger"); + PyXmlSec_SetLastError("failed to create xmlsecKeyManager"); return -1; } if (xmlSecCryptoAppDefaultKeysMngrInit(handle) < 0) { xmlSecKeysMngrDestroy(handle); - PyXmlSec_SetLastError("failed to initialize xmlsecKeyManger"); + PyXmlSec_SetLastError("failed to initialize xmlsecKeyManager"); return -1; } PYXMLSEC_DEBUGF("%p: init key manager - done: %p", self, handle); @@ -588,8 +686,10 @@ static void PyXmlSec_KeysManager__del__(PyObject* self) { } static const char PyXmlSec_KeysManagerAddKey__doc__[] = \ - "Adds a copy of *key* to keys manager\n\n" - ":param key: the pointer to key\n"; + "add_key(key: xmlsec.Key) -> None\n" + "Adds a copy of ``key`` to keys manager\n\n" + ":param key: the pointer to key\n" + ":type key: :class:`~xmlsec.Key`"; static PyObject* PyXmlSec_KeysManagerAddKey(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "key", NULL}; @@ -633,10 +733,14 @@ static PyObject* PyXmlSec_KeysManagerAddKey(PyObject* self, PyObject* args, PyOb } static const char PyXmlSec_KeysManagerLoadCert__doc__[] = \ - "Loads certificate from *filename*.\n\n" + "load_cert(filename, format, type) -> None\n" + "Loads certificate from ``filename``.\n\n" ":param filename: the certificate file\n" + ":type filename: :class:`str`, :class:`bytes` or any :class:`~os.PathLike`\n" ":param format: the certificate file format\n" - ":param type: the flag that indicates is the certificate in filename trusted or not\n"; + ":type format: :class:`int`\n" + ":param type: the flag that indicates is the certificate in filename trusted or not\n" + ":type type: :class:`int`"; static PyObject* PyXmlSec_KeysManagerLoadCert(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "filename", "format", "type", NULL}; @@ -650,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; } @@ -673,10 +777,14 @@ static PyObject* PyXmlSec_KeysManagerLoadCert(PyObject* self, PyObject* args, Py } static const char PyXmlSec_KeysManagerLoadCertFromMemory__doc__[] = \ - "Loads certificate from *data*\n\n" + "load_cert_from_memory(data, format, type) -> None\n" + "Loads certificate from ``data``\n\n" ":param data: the certificate binary data\n" + ":type data: :class:`str` or :class:`bytes`\n" ":param format: the certificate file format\n" - ":param type: the flag that indicates is the certificate in filename trusted or not\n"; + ":type format: :class:`int`\n" + ":param type: the flag that indicates is the certificate in filename trusted or not\n" + ":type type: :class:`int`"; static PyObject* PyXmlSec_KeysManagerLoadCertFromMemory(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "data", "format", "type", NULL}; 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 de98a0b7..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,14 +89,20 @@ 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; } static char PyXmlSec_PyInit__doc__[] = \ + "init() -> None\n" "Initializes the library for general operation.\n\n" "This is called upon library import and does not need to be called\n" - "again *shutdown* is called explicitly).\n"; + "again :func:`~.shutdown` is called explicitly).\n"; static PyObject* PyXmlSec_PyInit(PyObject *self) { if (PyXmlSec_Init() < 0) { return NULL; @@ -102,6 +111,7 @@ static PyObject* PyXmlSec_PyInit(PyObject *self) { } static char PyXmlSec_PyShutdown__doc__[] = \ + "shutdown() -> None\n" "Shutdowns the library and cleanup any leftover resources.\n\n" "This is called automatically upon interpreter termination and\n" "should not need to be called explicitly."; @@ -110,9 +120,42 @@ 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" - ":param enable_debug_trace: flag, debug trace is enabled or disabled"; + ":param enabled: flag, debug trace is enabled or disabled\n" + ":type enabled: :class:`bool`"; static PyObject* PyXmlSec_PyEnableDebugOutput(PyObject *self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "enabled", NULL}; PyObject* enabled = Py_True; @@ -123,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", @@ -136,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 */ }; @@ -163,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; @@ -185,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 */ } @@ -254,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 9a68dbd7..ae0eca34 100644 --- a/src/template.c +++ b/src/template.c @@ -18,14 +18,21 @@ #define PYXMLSEC_TEMPLATES_DOC "Xml Templates processing" static char PyXmlSec_TemplateCreate__doc__[] = \ - "Creates new node with the mandatory , ," - " and children and sub-children.\n\n" + "create(node, c14n_method, sign_method, id = None, ns = None) -> lxml.etree._Element\n" + "Creates new :xml:`` node with the mandatory :xml:``, :xml:``, " + ":xml:`` and :xml:`` children and sub-children.\n\n" ":param node: the signature node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param c14n_method: the signature canonicalization method\n" + ":type c14n_method: :class:`__Transform`\n" ":param sign_method: the signature method\n" + ":type sign_method: :class:`__Transform`\n" ":param id: the node id (optional)\n" - ":param ns: the namespace prefix for the signature element (e.g. \"dsig\") (optional)\n" - ":return: the pointer to newly created node\n"; + ":type id: :class:`str` or :data:`None`\n" + ":param ns: the namespace prefix for the signature element (e.g. ``\"dsig\"``) (optional)\n" + ":type ns: :class:`str` or :data:`None`\n" + ":return: the pointer to newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateCreate(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "c14n_method", "sign_method", "id", "ns", "name", NULL}; @@ -60,14 +67,21 @@ static PyObject* PyXmlSec_TemplateCreate(PyObject* self, PyObject *args, PyObjec } static char PyXmlSec_TemplateAddReference__doc__[] = \ - "Adds node with given URI (uri ) Id (id ) and Type (type ) attributes and\n" - "the required children and to the child of *node*.\n\n" - ":param node: the pointer to node\n" + "add_reference(node, digest_method, id = None, uri = None, type = None) -> lxml.etree._Element\n" + "Adds :xml:`` node with given ``\"URI\"`` (``uri``), ``\"Id\"`` (``id``) and ``\"Type\"`` (``type``) attributes and " + "the required children :xml:`` and :xml:`` to the :xml:`` child of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param digest_method: the reference digest method\n" + ":type digest_method: :class:`__Transform`\n" ":param id: the node id (optional)\n" - ":param uri: the reference node uri (optional)\n" + ":type id: :class:`str` or :data:`None`\n" + ":param uri: the reference node URI (optional)\n" + ":type uri: :class:`str` or :data:`None`\n" ":param type: the reference node type (optional)\n" - ":return: the pointer to newly created node\n"; + ":type type: :class:`str` or :data:`None`\n" + ":return: the pointer to newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddReference(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "digest_method", "id", "uri", "type", NULL}; @@ -101,10 +115,14 @@ static PyObject* PyXmlSec_TemplateAddReference(PyObject* self, PyObject *args, P } static char PyXmlSec_TemplateAddTransform__doc__[] = \ - "Adds node to the node of *node*.\n\n" - ":param node: the pointer to node\n" + "add_transform(node) -> lxml.etree._Element\n" + "Adds :xml:`` node to the :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param transform: the transform method id\n" - ":return: the pointer to newly created node\n"; + ":type transform: :class:`__Transform`\n" + ":return: the pointer to newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddTransform(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "transform", NULL}; @@ -135,10 +153,14 @@ static PyObject* PyXmlSec_TemplateAddTransform(PyObject* self, PyObject *args, P } static char PyXmlSec_TemplateEnsureKeyInfo__doc__[] = \ - "Adds (if necessary) node to the node of *node*.\n\n" - ":param node: the pointer to node\n" + "ensure_key_info(node, id = None) -> lxml.etree._Element\n" + "Adds (if necessary) :xml:`` node to the :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param id: the node id (optional)\n" - ":return: the pointer to newly created node\n"; + ":type id: :class:`str` or :data:`None`\n" + ":return: the pointer to newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateEnsureKeyInfo(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "id", NULL}; @@ -169,10 +191,14 @@ static PyObject* PyXmlSec_TemplateEnsureKeyInfo(PyObject* self, PyObject *args, } static char PyXmlSec_TemplateAddKeyName__doc__[] = \ - "Adds node to the node of *node*.\n\n" - ":param node: the pointer to node\n" + "add_key_name(node, name = None) -> lxml.etree._Element\n" + "Adds :xml:`` node to the :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param name: the key name (optional)\n" - ":return: the pointer to the newly created node\n"; + ":type name: :class:`str` or :data:`None`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddKeyName(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "name", NULL}; @@ -203,9 +229,12 @@ static PyObject* PyXmlSec_TemplateAddKeyName(PyObject* self, PyObject *args, PyO } static char PyXmlSec_TemplateAddKeyValue__doc__[] = \ - "Adds node to the node of *node*.\n\n" - ":param node: the pointer to node\n" - ":return: the pointer to the newly created node\n"; + "add_key_value(node) -> lxml.etree._Element\n" + "Adds :xml:`` node to the :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddKeyValue(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", NULL}; @@ -235,9 +264,12 @@ static PyObject* PyXmlSec_TemplateAddKeyValue(PyObject* self, PyObject *args, Py } static char PyXmlSec_TemplateAddX509Data__doc__[] = \ - "Adds node to the node of *node*.\n\n" - ":param node: the pointer to node\n" - ":return: the pointer to the newly created node\n"; + "add_x509_data(node) -> lxml.etree._Element\n" + "Adds :xml:`` node to the :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`\n"; static PyObject* PyXmlSec_TemplateAddX509Data(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", NULL}; @@ -267,9 +299,12 @@ static PyObject* PyXmlSec_TemplateAddX509Data(PyObject* self, PyObject *args, Py } static char PyXmlSec_TemplateAddX509DataAddIssuerSerial__doc__[] = \ - "Adds node to the given node of *node*.\n\n" - ":param node: the pointer to node\n" - ":return: the pointer to the newly created node\n"; + "x509_data_add_issuer_serial(node) -> lxml.etree._Element\n" + "Adds :xml:`` node to the given :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddX509DataAddIssuerSerial(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", NULL}; @@ -299,10 +334,14 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddIssuerSerial(PyObject* self, PyO } static char PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerName__doc__[] = \ - "Adds node to the node of *node*.\n\n" - ":param node: the pointer to node\n" + "x509_issuer_serial_add_issuer_name(node, name = None) -> lxml.etree._Element\n" + "Adds :xml:`` node to the :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param name: the issuer name (optional)\n" - ":return: the pointer to the newly created node\n"; + ":type name: :class:`str` or :data:`None`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerName(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "name", NULL}; @@ -334,10 +373,14 @@ static PyObject* PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerName(PyObject* } static char PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerSerialNumber__doc__[] = \ - "Adds node to the node of *node*.\n\n" - ":param node: the pointer to node\n" + "x509_issuer_serial_add_serial_number(node, serial = None) -> lxml.etree._Element\n" + "Adds :xml:`` node to the :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param serial: the serial number (optional)\n" - ":return: the pointer to the newly created node\n"; + ":type serial: :class:`str` or :data:`None`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerSerialNumber(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "serial", NULL}; @@ -369,9 +412,12 @@ static PyObject* PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerSerialNumber(P } static char PyXmlSec_TemplateAddX509DataAddSubjectName__doc__[] = \ - "Adds node to the given node of *node*.\n\n" - ":param node: the pointer to node\n" - ":return: the pointer to the newly created node\n"; + "x509_data_add_subject_name(node) -> lxml.etree._Element\n" + "Adds :xml:`` node to the given :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddX509DataAddSubjectName(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", NULL}; @@ -402,9 +448,12 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddSubjectName(PyObject* self, PyOb } static char PyXmlSec_TemplateAddX509DataAddSKI__doc__[] = \ - "Adds node to the given node of *node*.\n\n" - ":param node: the pointer to node\n" - ":return: the pointer to the newly created node\n"; + "x509_data_add_ski(node) -> lxml.etree._Element\n" + "Adds :xml:`` node to the given :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddX509DataAddSKI(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", NULL}; @@ -435,9 +484,12 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddSKI(PyObject* self, PyObject *ar } static char PyXmlSec_TemplateAddX509DataAddCertificate__doc__[] = \ - "Adds node to the given node of *node*.\n\n" - ":param node: the pointer to node\n" - ":return: the pointer to the newly created node\n"; + "x509_data_add_certificate(node) -> lxml.etree._Element\n" + "Adds :xml:`` node to the given :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddX509DataAddCertificate(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", NULL}; @@ -468,9 +520,12 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddCertificate(PyObject* self, PyOb } static char PyXmlSec_TemplateAddX509DataAddCRL__doc__[] = \ - "Adds node to the given node of *node*.\n\n" - ":param node: the pointer to node\n" - ":return: the pointer to the newly created node\n"; + "x509_data_add_crl(node) -> lxml.etree._Element\n" + "Adds :xml:`` node to the given :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddX509DataAddCRL(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", NULL}; @@ -501,13 +556,20 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddCRL(PyObject* self, PyObject *ar } static char PyXmlSec_TemplateAddEncryptedKey__doc__[] = \ - "Adds node with given attributes to the node of *node*.\n\n" - ":param node: the pointer to node\n" + "add_encrypted_key(node, method, id = None, type = None, recipient = None) -> lxml.etree._Element\n" + "Adds :xml:`` node with given attributes to the :xml:`` node of *node*.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param method: the encryption method\n" - ":param id: the Id attribute (optional)\n" - ":param type: the Type attribute (optional)\n" - ":param recipient: the Recipient attribute (optional)\n" - ":return: the pointer to the newly created node\n"; + ":type method: :class:`__Transform`\n" + ":param id: the ``\"Id\"`` attribute (optional)\n" + ":type id: :class:`str` or :data:`None`\n" + ":param type: the ``\"Type\"`` attribute (optional)\n" + ":type type: :class:`str` or :data:`None`\n" + ":param recipient: the ``\"Recipient\"`` attribute (optional)\n" + ":type recipient: :class:`str` or :data:`None`\n" + ":return: the pointer to the newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateAddEncryptedKey(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "method", "id", "type", "recipient", NULL}; @@ -542,15 +604,24 @@ static PyObject* PyXmlSec_TemplateAddEncryptedKey(PyObject* self, PyObject *args } static char PyXmlSec_TemplateCreateEncryptedData__doc__[] = \ - "Creates new <{ns}:EncryptedData /> node for encryption template.\n\n" + "encrypted_data_create(node, method, id = None, type = None, mime_type = None, encoding = None, ns = None) -> lxml.etree._Element\n" + "Creates new :xml:`<{ns}:EncryptedData />` node for encryption template.\n\n" ":param node: the pointer to signature node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param method: the encryption method\n" - ":param id: the Id attribute (optional)\n" - ":param type: the Type attribute (optional)\n" - ":param mime_type: the Recipient attribute (optional)\n" - ":param encoding: the MimeType attribute (optional)\n" + ":type method: :class:`__Transform`\n" + ":param id: the ``\"Id\"`` attribute (optional)\n" + ":type id: :class:`str` or :data:`None`\n" + ":param type: the ``\"Type\"`` attribute (optional)\n" + ":type type: :class:`str` or :data:`None`\n" + ":param mime_type: the ``\"Recipient\"`` attribute (optional)\n" + ":type mime_type: :class:`str` or :data:`None`\n" + ":param encoding: the ``\"MimeType\"`` attribute (optional)\n" + ":type encoding: :class:`str` or :data:`None`\n" ":param ns: the namespace prefix (optional)\n" - ":return: the pointer newly created node\n"; + ":type ns: :class:`str` or :data:`None`\n" + ":return: the pointer newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateCreateEncryptedData(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "method", "id", "type", "mime_type", "encoding", "ns", NULL}; @@ -590,11 +661,16 @@ static PyObject* PyXmlSec_TemplateCreateEncryptedData(PyObject* self, PyObject * } static char PyXmlSec_TemplateEncryptedDataEnsureKeyInfo__doc__[] = \ - "Adds <{ns}:KeyInfo/> to the node of *node*.\n\n" - ":param node: the pointer to node\n" - ":param id: the Id attribute (optional)\n" + "encrypted_data_ensure_key_info(node, id = None, ns = None) -> lxml.etree._Element\n" + "Adds :xml:`<{ns}:KeyInfo/>` to the :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":param id: the ``\"Id\"`` attribute (optional)\n" + ":type id: :class:`str` or :data:`None`\n" ":param ns: the namespace prefix (optional)\n" - ":return: the pointer to newly created node\n"; + ":type ns: :class:`str` or :data:`None`\n" + ":return: the pointer to newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateEncryptedDataEnsureKeyInfo(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "id", "ns", NULL}; @@ -630,9 +706,12 @@ static PyObject* PyXmlSec_TemplateEncryptedDataEnsureKeyInfo(PyObject* self, PyO } static char PyXmlSec_TemplateEncryptedDataEnsureCipherValue__doc__[] = \ - "Adds to the node of *node*.\n\n" - ":param node: the pointer to node\n" - ":return: the pointer to newly created node\n"; + "encrypted_data_ensure_cipher_value(node) -> lxml.etree._Element\n" + "Adds :xml:`` to the :xml:`` node of ``node``.\n\n" + ":param node: the pointer to :xml:`` node\n" + ":type node: :class:`lxml.etree._Element`\n" + ":return: the pointer to newly created :xml:`` node\n" + ":rtype: :class:`lxml.etree._Element`"; static PyObject* PyXmlSec_TemplateEncryptedDataEnsureCipherValue(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", NULL}; @@ -663,9 +742,12 @@ static PyObject* PyXmlSec_TemplateEncryptedDataEnsureCipherValue(PyObject* self, } static char PyXmlSec_TemplateTransformAddC14NInclNamespaces__doc__[] = \ - "Adds 'inclusive' namespaces to the ExcC14N transform node *node*.\n\n" - ":param node: the pointer to node.\n" - ":param prefixList: the list of namespace prefixes, where 'default' indicates the default namespace (optional)."; + "transform_add_c14n_inclusive_namespaces(node, prefixes = None) -> None\n" + "Adds 'inclusive' namespaces to the ExcC14N transform node ``node``.\n\n" + ":param node: the pointer to :xml:`` node.\n" + ":type node: :class:`lxml.etree._Element`\n" + ":param prefixes: the list of namespace prefixes, where ``'default'`` indicates the default namespace (optional).\n" + ":type prefixes: :class:`str` or :class:`list` of strings"; static PyObject* PyXmlSec_TemplateTransformAddC14NInclNamespaces(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "prefixes", NULL}; @@ -684,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"); @@ -699,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; @@ -836,7 +918,6 @@ static PyMethodDef PyXmlSec_TemplateMethods[] = { {NULL, NULL} /* sentinel */ }; -#ifdef PY3K static PyModuleDef PyXmlSec_TemplateModule = { PyModuleDef_HEAD_INIT, @@ -849,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 ac23b53f..37cae785 100644 --- a/src/tree.c +++ b/src/tree.c @@ -16,11 +16,16 @@ #define PYXMLSEC_TREE_DOC "Common XML utility functions" static char PyXmlSec_TreeFindChild__doc__[] = \ - "Searches a direct child of the parent node having given name and namespace href.\n\n" + "find_child(parent, name, namespace)\n" + "Searches a direct child of the ``parent`` node having given ``name`` and ``namespace`` href.\n\n" ":param parent: the pointer to XML node\n" + ":type parent: :class:`lxml.etree._Element`\n" ":param name: the name\n" - ":param namespace: the namespace href(optional)\n" - ":return: the pointer to the found node or None if node is not found\n"; + ":type name: :class:`str`\n" + ":param namespace: the namespace href (optional)\n" + ":type namespace: :class:`str`\n" + ":return: the pointer to the found node or :data:`None` if node is not found\n" + ":rtype: :class:`lxml.etree._Element` or :data:`None`"; static PyObject* PyXmlSec_TreeFindChild(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "parent", "name", "namespace", NULL}; @@ -52,11 +57,16 @@ static PyObject* PyXmlSec_TreeFindChild(PyObject* self, PyObject *args, PyObject } static char PyXmlSec_TreeFindParent__doc__[] = \ - "Searches the ancestors axis of the node having given name and namespace href.\n\n" + "find_parent(node, name, namespace)\n" + "Searches the ancestors axis of the ``node`` having given ``name`` and ``namespace`` href.\n\n" ":param node: the pointer to XML node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param name: the name\n" - ":param namespace: the namespace href(optional)\n" - ":return: the pointer to the found node or None if node is not found\n"; + ":type name: :class:`str`\n" + ":param namespace: the namespace href (optional)\n" + ":type namespace: :class:`str`\n" + ":return: the pointer to the found node or :data:`None` if node is not found\n" + ":rtype: :class:`lxml.etree._Element` or :data:`None`"; static PyObject* PyXmlSec_TreeFindParent(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "name", "namespace", NULL}; @@ -88,11 +98,16 @@ static PyObject* PyXmlSec_TreeFindParent(PyObject* self, PyObject *args, PyObjec } static char PyXmlSec_TreeFindNode__doc__[] = \ - "Searches all children of the parent node having given name and namespace href.\n\n" + "find_node(node, name, namespace)\n" + "Searches all children of the given ``node`` having given ``name`` and ``namespace`` href.\n\n" ":param node: the pointer to XML node\n" + ":type node: :class:`lxml.etree._Element`\n" ":param name: the name\n" - ":param namespace: the namespace href(optional)\n" - ":return: the pointer to the found node or None if node is not found\n"; + ":type name: :class:`str`\n" + ":param namespace: the namespace href (optional)\n" + ":type namespace: :class:`str`\n" + ":return: the pointer to the found node or :data:`None` if node is not found\n" + ":rtype: :class:`lxml.etree._Element` or :data:`None`"; static PyObject* PyXmlSec_TreeFindNode(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "name", "namespace", NULL}; @@ -124,13 +139,16 @@ static PyObject* PyXmlSec_TreeFindNode(PyObject* self, PyObject *args, PyObject } static char PyXmlSec_TreeAddIds__doc__[] = \ - "Registers *ids* as ids used below *node*. *ids* is a sequence of attribute names\n"\ - "used as XML ids in the subtree rooted at *node*.\n"\ - "A call to `addIds` may be necessary to make known which attributes contain XML ids.\n"\ - "This is the case, if a transform references an id via `XPointer` or a self document uri and\n" + "add_ids(node, ids) -> None\n" + "Registers ``ids`` as ids used below ``node``. ``ids`` is a sequence of attribute names "\ + "used as XML ids in the subtree rooted at ``node``.\n"\ + "A call to :func:`~.add_ids` may be necessary to make known which attributes contain XML ids.\n"\ + "This is the case, if a transform references an id via ``XPointer`` or a self document uri and " "the id inkey_data_formation is not available by other means (e.g. an associated DTD or XML schema).\n\n" ":param node: the pointer to XML node\n" - ":param ids: the list of ID attributes.\n"; + ":type node: :class:`lxml.etree._Element`\n" + ":param ids: the list of ID attributes.\n" + ":type ids: :class:`list` of strings"; static PyObject* PyXmlSec_TreeAddIds(PyObject* self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = { "node", "ids", NULL}; @@ -164,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; } @@ -212,7 +230,6 @@ static PyMethodDef PyXmlSec_TreeMethods[] = { {NULL, NULL} /* sentinel */ }; -#ifdef PY3K static PyModuleDef PyXmlSec_TreeModule = { PyModuleDef_HEAD_INIT, @@ -225,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 new file mode 100644 index 00000000..9cfc8cc6 --- /dev/null +++ b/src/xmlsec/__init__.pyi @@ -0,0 +1,80 @@ +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 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) + +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: 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: ... + def encrypt_xml(self, template: _E, node: _Element) -> _E: ... + def reset(self) -> None: ... + +class Error(Exception): ... +class InternalError(Error): ... + +class Key: + name: str + @classmethod + 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_file(cls: type[Self], file: GenericPath[AnyStr] | IO[AnyStr], format: int, password: str | None = ...) -> Self: ... + @classmethod + def from_engine(cls: type[Self], engine_and_key_id: AnyStr) -> Self: ... + @classmethod + def from_memory(cls: type[Self], data: AnyStr, format: int, password: str | None = ...) -> Self: ... + @classmethod + 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: Self) -> Self: ... + def __deepcopy__(self: Self) -> Self: ... + +class KeysManager: + def add_key(self, key: Key) -> 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: 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_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: ... + def verify(self, node: _Element) -> None: ... + def verify_binary(self, bytes: bytes, transform: Transform, signature: bytes) -> None: ... + +class VerificationError(Error): ... diff --git a/src/xmlsec/constants.pyi b/src/xmlsec/constants.pyi new file mode 100644 index 00000000..a9254ddd --- /dev/null +++ b/src/xmlsec/constants.pyi @@ -0,0 +1,148 @@ +import sys +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 + +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/py.typed b/src/xmlsec/py.typed new file mode 100644 index 00000000..775794d8 --- /dev/null +++ b/src/xmlsec/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. The xmlsec package uses stub files. diff --git a/src/xmlsec/template.pyi b/src/xmlsec/template.pyi new file mode 100644 index 00000000..d1755fa2 --- /dev/null +++ b/src/xmlsec/template.pyi @@ -0,0 +1,38 @@ +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: str | None = ..., type: str | None = ..., recipient: str | None = ... +) -> _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: 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, id: str | None = ..., ns: str | None = ...) -> _Element: ... +def encrypted_data_create( + node: _Element, + method: Transform, + 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: 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: 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 new file mode 100644 index 00000000..9f96e447 --- /dev/null +++ b/src/xmlsec/tree.pyi @@ -0,0 +1,18 @@ +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) -> _Element | None: ... +@overload +def find_child(parent: _Element, name: str, namespace: str = ...) -> _Element | None: ... +@overload +def find_node(node: _Element, name: str) -> _Element | None: ... +@overload +def find_node(node: _Element, name: str, namespace: str = ...) -> _Element | None: ... +@overload +def find_parent(node: _Element, name: str) -> _Element | None: ... +@overload +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 new file mode 100644 index 00000000..2c39d5f6 --- /dev/null +++ b/tests/test_constants.py @@ -0,0 +1,42 @@ +"""Test constants from :mod:`xmlsec.constants` module.""" + +import pytest + +import xmlsec + + +def _constants(typename): + return list( + sorted( + ( + getattr(xmlsec.constants, name) + for name in dir(xmlsec.constants) + if type(getattr(xmlsec.constants, name)).__name__ == typename + ), + key=lambda t: t.name.lower(), + ) + ) + + +@pytest.mark.parametrize('transform', _constants('__Transform'), ids=repr) +def test_transform_str(transform): + """Test string representation of ``xmlsec.constants.__Transform``.""" + assert str(transform) == f'{transform.name}, {transform.href}' + + +@pytest.mark.parametrize('transform', _constants('__Transform'), ids=repr) +def test_transform_repr(transform): + """Test raw string representation of ``xmlsec.constants.__Transform``.""" + assert repr(transform) == f'__Transform({transform.name!r}, {transform.href!r}, {transform.usage})' + + +@pytest.mark.parametrize('keydata', _constants('__KeyData'), ids=repr) +def test_keydata_str(keydata): + """Test string representation of ``xmlsec.constants.__KeyData``.""" + assert str(keydata) == f'{keydata.name}, {keydata.href}' + + +@pytest.mark.parametrize('keydata', _constants('__KeyData'), ids=repr) +def test_keydata_repr(keydata): + """Test raw string representation of ``xmlsec.constants.__KeyData``.""" + 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 new file mode 100644 index 00000000..7aa8e517 --- /dev/null +++ b/tests/test_doc_examples.py @@ -0,0 +1,36 @@ +"""Run tests over code examples in the documentation.""" + +import contextlib +import os +import runpy +from pathlib import Path + +import pytest + +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. + + Restore the current working dir after exiting the context. + """ + curr = Path.cwd() + try: + os.chdir(str(where_to)) + yield + finally: + os.chdir(str(curr)) + + +@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. + + Execute each script in :file:`docs/source/examples`, + not raising any errors is good enough. + """ + with cd(example.parent): + runpy.run_path(str(example)) 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 new file mode 100644 index 00000000..82f7df7f --- /dev/null +++ b/tests/test_type_stubs.py @@ -0,0 +1,70 @@ +"""Test type stubs for correctness where possible.""" + +import os + +import pytest + +import xmlsec + +black = pytest.importorskip('black') + + +constants_stub_header = """ +import sys +from typing import NamedTuple + +if sys.version_info >= (3, 8): + from typing import Final +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`. + + Simply load all constants at runtime, + generate appropriate type hint for each constant type. + """ + + def process_constant(name): + """Generate line in stub file for constant name.""" + obj = getattr(xmlsec.constants, name) + 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] + return constants_stub_header + os.linesep.join(lines) + + +def test_xmlsec_constants_stub(request): + """Generate the stub file for :mod:`xmlsec.constants` from existing code. + + Compare it against the existing stub :file:`xmlsec/constants.pyi`. + """ + 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/version.txt b/version.txt deleted file mode 100644 index e7526496..00000000 --- a/version.txt +++ /dev/null @@ -1 +0,0 @@ -1.0.1.dev0 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')