diff --git a/.github/scripts/manylinux_build_and_test.sh b/.github/scripts/manylinux_build_and_test.sh new file mode 100644 index 00000000..01c6867f --- /dev/null +++ b/.github/scripts/manylinux_build_and_test.sh @@ -0,0 +1,52 @@ +#!/bin/sh +set -eu + +: "${PY_ABI:?PY_ABI is required}" +: "${MANYLINUX_IMAGE:?MANYLINUX_IMAGE is required}" + +# Make local build helpers importable for isolated PEP 517 backend subprocesses. +export PYTHONPATH="$PWD${PYTHONPATH:+:${PYTHONPATH}}" +# Ensure dependency archives are read from the restored workspace cache even in isolated builds. +export PYXMLSEC_LIBS_DIR="$PWD/libs" + +# Step: Install system build dependencies (manylinux only) +echo "== [container] Step: Install system build dependencies (manylinux only) ==" +case "$MANYLINUX_IMAGE" in + manylinux*) + yum install -y perl-core + ;; +esac + +# Step: Install python build dependencies +echo "== [container] Step: Install python build dependencies ==" +/opt/python/${PY_ABI}/bin/pip install --upgrade pip setuptools wheel build 'setuptools_scm>=8' + +# Step: Set environment variables +echo "== [container] Step: Set environment variables ==" +PKGVER=$(/opt/python/${PY_ABI}/bin/python setup.py --version) +echo "PKGVER=$PKGVER" + +# Step: Build linux_x86_64 wheel +echo "== [container] Step: Build linux_x86_64 wheel ==" +/opt/python/${PY_ABI}/bin/python -m build + +# Step: Label manylinux wheel +echo "== [container] Step: Label manylinux wheel ==" +ls -la dist/ +auditwheel show dist/xmlsec-${PKGVER}-${PY_ABI}-linux_x86_64.whl +auditwheel repair dist/xmlsec-${PKGVER}-${PY_ABI}-linux_x86_64.whl +ls -la wheelhouse/ +auditwheel show wheelhouse/xmlsec-${PKGVER}-${PY_ABI}-*${MANYLINUX_IMAGE}*.whl + +# Step: Install test dependencies +echo "== [container] Step: Install test dependencies ==" +/opt/python/${PY_ABI}/bin/pip install --upgrade -r requirements-test.txt +/opt/python/${PY_ABI}/bin/pip install xmlsec --only-binary=xmlsec --no-index --find-links=wheelhouse/ + +# Step: Run tests +echo "== [container] Step: Run tests ==" +/opt/python/${PY_ABI}/bin/pytest -v --color=yes + +# Step: Fix mounted workspace file ownership on host +echo "== [container] Step: Fix mounted workspace file ownership on host ==" +chown -R "${HOST_UID}:${HOST_GID}" dist wheelhouse build libs || true diff --git a/.github/workflows/cache_libs.yml b/.github/workflows/cache_libs.yml new file mode 100644 index 00000000..77c83353 --- /dev/null +++ b/.github/workflows/cache_libs.yml @@ -0,0 +1,134 @@ +name: Cache library dependencies + +on: + workflow_call: + inputs: + LIBICONV_VERSION: + default: "1.18" + required: false + type: string + LIBXML2_VERSION: + default: "2.14.6" + required: false + type: string + LIBXSLT_VERSION: + default: "1.1.43" + required: false + type: string + OPENSSL_VERSION: + default: "3.6.0" + required: false + type: string + XMLSEC1_VERSION: + default: "1.3.9" + required: false + type: string + ZLIB_VERSION: + default: "1.3.1" + required: false + type: string + WIN_LIBICONV_VERSION: + default: "1.18-1" + required: false + type: string + WIN_LIBXML2_VERSION: + default: "2.11.9-3" + required: false + type: string + WIN_LIBXSLT_VERSION: + default: "1.1.39" + required: false + type: string + WIN_OPENSSL_VERSION: + default: "3.0.16.pl1" + required: false + type: string + WIN_XMLSEC1_VERSION: + default: "1.3.7" + required: false + type: string + WIN_ZLIB_VERSION: + default: "1.3.1" + required: false + type: string + + outputs: + LIBICONV_VERSION: + value: ${{ inputs.LIBICONV_VERSION }} + LIBXML2_VERSION: + value: ${{ inputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: + value: ${{ inputs.LIBXSLT_VERSION }} + OPENSSL_VERSION: + value: ${{ inputs.OPENSSL_VERSION }} + XMLSEC1_VERSION: + value: ${{ inputs.XMLSEC1_VERSION }} + ZLIB_VERSION: + value: ${{ inputs.ZLIB_VERSION }} + WIN_LIBICONV_VERSION: + value: ${{ inputs.WIN_LIBICONV_VERSION }} + WIN_LIBXML2_VERSION: + value: ${{ inputs.WIN_LIBXML2_VERSION }} + WIN_LIBXSLT_VERSION: + value: ${{ inputs.WIN_LIBXSLT_VERSION }} + WIN_OPENSSL_VERSION: + value: ${{ inputs.WIN_OPENSSL_VERSION }} + WIN_XMLSEC1_VERSION: + value: ${{ inputs.WIN_XMLSEC1_VERSION }} + WIN_ZLIB_VERSION: + value: ${{ inputs.WIN_ZLIB_VERSION }} + +jobs: + cache_libs: + strategy: + fail-fast: false + matrix: + os: + - "ubuntu-22.04" + - "ubuntu-22.04-arm" + - "macos-latest" + - "windows-2022" + - "windows-11-arm" + + runs-on: ${{ matrix.os }} + + env: + LIBICONV_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_LIBICONV_VERSION || inputs.LIBICONV_VERSION }} + LIBXML2_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_LIBXML2_VERSION || inputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_LIBXSLT_VERSION || inputs.LIBXSLT_VERSION }} + OPENSSL_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_OPENSSL_VERSION || inputs.OPENSSL_VERSION }} + XMLSEC1_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_XMLSEC1_VERSION || inputs.XMLSEC1_VERSION }} + ZLIB_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_ZLIB_VERSION || inputs.ZLIB_VERSION }} + + steps: + - uses: actions/checkout@v6 + + - name: Cache [libs] + uses: actions/cache@v4.3.0 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} + + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Install setuptools shim + run: python -m pip install --upgrade pip setuptools + + - name: Download latest libraries + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python build_libs_xmlsec.py --download-only + + - name: Check Windows library versions + if: ${{ contains(matrix.os, 'windows-') }} + run: | + bash -c ' + for file in libs/iconv-${{ inputs.WIN_LIBICONV_VERSION }}.*.zip libs/libxml2-${{ inputs.WIN_LIBXML2_VERSION }}.*.zip libs/libxslt-${{ inputs.WIN_LIBXSLT_VERSION }}.*.zip libs/openssl-${{ inputs.WIN_OPENSSL_VERSION }}.*.zip libs/xmlsec-${{ inputs.WIN_XMLSEC1_VERSION }}.*.zip libs/zlib-${{ inputs.WIN_ZLIB_VERSION }}.*.zip; do + [[ -f "$file" ]] || { echo "MISSING: $file" ; exit 1; } + done + ' diff --git a/.github/workflows/linuxbrew.yml b/.github/workflows/linuxbrew.yml new file mode 100644 index 00000000..51b0db1e --- /dev/null +++ b/.github/workflows/linuxbrew.yml @@ -0,0 +1,50 @@ +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..522a2a0a --- /dev/null +++ b/.github/workflows/macosx.yml @@ -0,0 +1,90 @@ +name: macOS +on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'master' }} + +jobs: + cache_libs: + uses: ./.github/workflows/cache_libs.yml + secrets: inherit + + macosx: + needs: cache_libs + runs-on: macos-latest + + env: + LIBXML2_VERSION: ${{ needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ needs.cache_libs.outputs.LIBXSLT_VERSION }} + + strategy: + matrix: + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + static_deps: ["static", ""] + + steps: + - uses: actions/checkout@v5.0.0 + + - name: Cache [libs] + id: cache-libs + uses: actions/cache/restore@v4.3.0 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} + + - 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_LIBS_DIR="$PWD/libs" + 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 (static only) + if: matrix.static_deps == 'static' + run: | + pip install coverage --upgrade -r requirements-test.txt + pip install xmlsec --only-binary=xmlsec --no-index --find-links=dist/ + + + - name: Install test dependencies (non-static only) + if: matrix.static_deps != 'static' + 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 + if: matrix.static_deps != 'static' + 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 diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml new file mode 100644 index 00000000..ff04f9ae --- /dev/null +++ b/.github/workflows/manylinux.yml @@ -0,0 +1,62 @@ +name: manylinux +on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'master' }} + +jobs: + cache_libs: + uses: ./.github/workflows/cache_libs.yml + secrets: inherit + + manylinux: + needs: cache_libs + runs-on: ubuntu-latest + + env: + LIBXML2_VERSION: ${{ needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ needs.cache_libs.outputs.LIBXSLT_VERSION }} + + 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 + + steps: + - uses: actions/checkout@v5.0.0 + with: + fetch-depth: 0 + + - name: Cache [libs] + uses: actions/cache/restore@v4.3.0 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} + + # Keep this job on the host runner so JS-based actions (for example actions/cache) + # can run, then execute build/test inside the target manylinux/musllinux container. + - name: Build and test in container + env: + PYXMLSEC_STATIC_DEPS: true + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PY_ABI: ${{ matrix.python-abi }} + MANYLINUX_IMAGE: ${{ matrix.image }} + run: | + set -euxo pipefail + docker run --rm \ + -v "$PWD:$PWD" \ + -w "$PWD" \ + -e GH_TOKEN \ + -e PYXMLSEC_STATIC_DEPS \ + -e PY_ABI \ + -e MANYLINUX_IMAGE \ + -e HOST_UID="$(id -u)" \ + -e HOST_GID="$(id -g)" \ + "quay.io/pypa/${MANYLINUX_IMAGE}" \ + sh .github/scripts/manylinux_build_and_test.sh diff --git a/.github/workflows/sdist.yml b/.github/workflows/sdist.yml new file mode 100644 index 00000000..f48ca02c --- /dev/null +++ b/.github/workflows/sdist.yml @@ -0,0 +1,43 @@ +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 + + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install build dependencies + run: | + pip install --upgrade pip setuptools wheel 'setuptools_scm>=8' + + - 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..fa80a645 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,153 @@ +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: + cache_libs: + uses: ./.github/workflows/cache_libs.yml + secrets: inherit + + 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@v5.0.0 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v6.0.0 + with: + python-version: '3.14' + + - name: Install build dependencies + run: | + pip install --upgrade pip setuptools wheel setuptools_scm>=8 pkgconfig>=1.5.1 + + - 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@v5.0.0 + 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-24.04 + + outputs: + include: ${{ steps.set-matrix.outputs.include }} + + steps: + - uses: actions/checkout@v5.0.0 + + - name: Install cibuildwheel + # N.B. Keep cibuildwheel version pin consistent with "build_wheels" job below. + run: pipx install cibuildwheel==3.3 + + - id: set-matrix + run: | + MATRIX=$( + { + cibuildwheel --print-build-identifiers --platform linux \ + | jq -nRc '{"only": inputs, "os": "ubuntu-22.04"}' \ + | sed -e '/aarch64/s|ubuntu-22.04|ubuntu-22.04-arm|' \ + && 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: [cache_libs, generate_wheels_matrix] + runs-on: ${{ matrix.os }} + + env: + LIBXML2_VERSION: ${{ contains(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_LIBXML2_VERSION || needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ contains(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_LIBXSLT_VERSION || needs.cache_libs.outputs.LIBXSLT_VERSION }} + + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.generate_wheels_matrix.outputs.include) }} + + steps: + - name: Check out the repo + uses: actions/checkout@v5.0.0 + with: + fetch-depth: 0 + + - name: Cache [libs] + uses: actions/cache/restore@v4.3.0 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v3.7.0 + with: + platforms: all + + - name: Build wheels + uses: pypa/cibuildwheel@v3.3.0 + with: + only: ${{ matrix.only }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload wheels + uses: actions/upload-artifact@v5.0.0 + with: + path: ./wheelhouse/*.whl + name: xmlsec-wheel-${{ matrix.only }} diff --git a/.gitignore b/.gitignore index f39e8aba..15f47985 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,10 @@ .* !.editorconfig !.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 5555e652..8d3ca07e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,32 +1,53 @@ -dist: trusty -sudo: false +dist: focal language: python +travis: + auto_cancel: + push: true + pull_request: true + +notifications: + email: false + python: -- '2.7' -- '3.4' -- '3.5' + - "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: -- sed -i "s/1.0.1/${TRAVIS_TAG:-1.0.1}/" setup.py + - 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 tests -deploy: - provider: pypi - user: bgaifullin - password: - secure: V7KMY7sTMk1zIEfJ4j9UiGWqwYRZHfeZ6qPt7lfRLF8QOE244/17/wYJRZhejk53YvRUMTK4Xh6tSOiwH9QRoI5vgcX5JJ5AQgI8xscidIDnxn9f/LOslp2LFLz+xM8XPmnqILclekA3T1RictbdKDuMFMY+MWgTSJu0QTOeUBM+LtbhqZKMr29T33EsVBdCjB77lw/2BqVlbo4TV9hxohbXth4OUd5wZdEMsDvrvySC1FUR4V/jzkcNrtxMnLjiDhvcapCfGwkEldCAI/liYulDQP3A3LZ7oeP04LvAO1XxL1Pq4VbnpENXYvtRL4mCIW8s8tDwYSsHi7633b6UIFVyjGegDboZCUDL1T86C54Sw8nLFSkNCBMXQOWDb1Sjilr80TYscaNXZl5BOI1XiOdtHRCwRkdpYr+UsfK3qgv8nsM+bCxTN4rDa/IfbcSWyM8OyLHbRQrXATgjXPwTCiURZVjsNEllUdSP0QH1jqrpapofbQV5nFVxohoF4UZVqJVYT6fljyyLPpjOgXWFU64m5L++y3hzYD1qeaCcm7TwhuF2k0IxT1j0Ze61MzufULHJ8BonGjBrgZG59Xk5JXKJWb+W+5DoodUK12hprBItH7EH7Ku3DE5QG+RHr18ivloMnApWggqBxdKb6bg6mXT0SekZbXmyTG7arEga4TA= - on: - tags: true - distributions: sdist bdist_wheel - python: '3.5' + - 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 -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 07ed1203..6c47dc9c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,14 @@ -include src/* -include requirements*.txt -include LICENSE - +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 53d427f5..00000000 --- a/README.rst +++ /dev/null @@ -1,179 +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://img.shields.io/pypi/v/xmlsec.svg - :target: https://pypi.python.org/pypi/xmlsec -.. image:: https://img.shields.io/pypi/dm/xmlsec.svg - :target: https://pypi.python.org/pypi/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 dowload 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 - - -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. - -Manual ------- - -1. Clone the **xmlsec** repository to your local computer. - -.. code-block:: bash - - git clone git://github.com/mehcode/python-xmlsec.git - -2. Change into the **xmlsec** root directory. - -.. code-block:: bash - - cd /path/to/xmlsec - - -3. Install the project and all its dependencies using `pip`. - -.. code-block:: bash - - pip install . - - -************ -Contributing -************ - -Setting up your environment ---------------------------- - -1. Follow steps 1 and 2 of the [manual installation instructions][]. - -[manual installation instructions]: #manual - -2. 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: http://virtualenvwrapper.readthedocs.org/en/latest/install.html#basic-installation - -3. 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 ----------------------- - -1. [Set up your environment](#setting-up-your-environment). - -2. Run the unit tests. - -.. code-block:: bash - - py.test tests - -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) - -******* -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_libs_xmlsec.py b/build_libs_xmlsec.py new file mode 100644 index 00000000..a4a7e603 --- /dev/null +++ b/build_libs_xmlsec.py @@ -0,0 +1,55 @@ +import argparse +import os +import sys +from pathlib import Path + +from build_support.lib_xmlsec_dependency_builder import LibXmlsecDependencyBuilder + + +def _console_info(message): + print(message) + + +def main(argv=None): + parser = argparse.ArgumentParser(description='Download and build static dependency libraries for python-xmlsec.') + parser.add_argument( + '--platform', + default=sys.platform, + help='Target platform (default: current interpreter platform).', + ) + parser.add_argument( + '--plat-name', + default=os.environ.get('PYXMLSEC_PLAT_NAME'), + help='Target platform tag for cross-compiling (for example macosx-11.0-arm64).', + ) + parser.add_argument( + '--libs-dir', + default=os.environ.get('PYXMLSEC_LIBS_DIR', 'libs'), + help='Directory where source/binary archives are stored.', + ) + parser.add_argument( + '--buildroot', + default=Path('build', 'tmp'), + type=Path, + help='Build root for extracted/build artifacts.', + ) + parser.add_argument( + '--download-only', + action='store_true', + help='Only download dependency archives; do not extract/build.', + ) + + args = parser.parse_args(argv) + builder = LibXmlsecDependencyBuilder( + platform_name=args.platform, + info=_console_info, + libs_dir=Path(args.libs_dir), + buildroot=args.buildroot, + plat_name=args.plat_name, + ) + builder.prepare(download_only=args.download_only) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tests/examples/__init__.py b/build_support/__init__.py similarity index 100% rename from tests/examples/__init__.py rename to build_support/__init__.py diff --git a/build_support/build_ext.py b/build_support/build_ext.py new file mode 100644 index 00000000..262acd97 --- /dev/null +++ b/build_support/build_ext.py @@ -0,0 +1,89 @@ +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 __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + 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) + + def info(self, message) -> None: + self.announce(message, level=log.INFO) + + def run(self) -> None: + ext = self.ext_map['xmlsec'] + 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/lib_xmlsec_dependency_builder.py b/build_support/lib_xmlsec_dependency_builder.py new file mode 100644 index 00000000..00b519dd --- /dev/null +++ b/build_support/lib_xmlsec_dependency_builder.py @@ -0,0 +1,417 @@ +import multiprocessing +import os +import platform +import subprocess +import sys +import tarfile +import zipfile +from dataclasses import dataclass +from distutils.errors import DistutilsError +from pathlib import Path +from typing import ClassVar +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, +) + + +@dataclass +class CrossCompileInfo: + host: str + arch: str + compiler: str + + @property + def triplet(self) -> str: + return f'{self.host}-{self.arch}-{self.compiler}' + + +class LibXmlsecDependencyBuilder: + WINDOWS_LIBS_DOWNLOAD_RELEASE_URL = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2025.07.10/' + LIB_VERSION_ENV_VARS: ClassVar[dict[str, str]] = { + 'libiconv_version': 'PYXMLSEC_LIBICONV_VERSION', + 'libxml2_version': 'PYXMLSEC_LIBXML2_VERSION', + 'libxslt_version': 'PYXMLSEC_LIBXSLT_VERSION', + 'openssl_version': 'PYXMLSEC_OPENSSL_VERSION', + 'xmlsec1_version': 'PYXMLSEC_XMLSEC1_VERSION', + 'zlib_version': 'PYXMLSEC_ZLIB_VERSION', + } + UNIX_DEFAULT_LIB_VERSIONS: ClassVar[dict[str, str]] = { + 'libiconv_version': '1.18', + 'libxml2_version': '2.14.6', # Make sure it matches with lxml + 'libxslt_version': '1.1.43', + 'openssl_version': '3.6.0', + 'xmlsec1_version': '1.3.9', + 'zlib_version': '1.3.1', + } + WINDOWS_DEFAULT_LIB_VERSIONS: ClassVar[dict[str, str]] = { + 'libiconv_version': '1.18-1', + 'libxml2_version': '2.11.9-3', # Make sure it matches with lxml + 'libxslt_version': '1.1.39', + 'openssl_version': '3.0.16.pl1', + 'xmlsec1_version': '1.3.7', + 'zlib_version': '1.3.1', + } + + def __init__(self, platform_name, info=None, libs_dir=None, buildroot=None, plat_name=None): + self.platform_name = platform_name + self.info = info or print + self.plat_name = plat_name + + self._prepare_directories(libs_dir=libs_dir, buildroot=buildroot) + self._set_library_versions(build_platform=platform_name) + + @property + def versions(self): + return {attr: getattr(self, attr) for attr in self.LIB_VERSION_ENV_VARS} + + def prepare(self, download_only=False): + self.info(f'preparing dependency build on {self.platform_name}') + if self.platform_name == 'win32': + self._prepare_windows_build(download_only=download_only) + elif 'linux' in self.platform_name or 'darwin' in self.platform_name: + self._prepare_unix_build(build_platform=self.platform_name, download_only=download_only) + else: + raise DistutilsError(f'Unsupported static build platform: {self.platform_name}') + + def _prepare_directories(self, libs_dir=None, buildroot=None): + buildroot_path = Path(buildroot) if buildroot else Path('build', 'tmp') + + prefix_dir = buildroot_path / 'prefix' + prefix_dir.mkdir(parents=True, exist_ok=True) + self.prefix_dir = prefix_dir.absolute() + + build_libs_dir = buildroot_path / 'libs' + build_libs_dir.mkdir(parents=True, exist_ok=True) + self.build_libs_dir = build_libs_dir + + libs_root = libs_dir if libs_dir is not None else os.environ.get('PYXMLSEC_LIBS_DIR', 'libs') + libs_dir_path = Path(libs_root) + libs_dir_path.mkdir(parents=True, exist_ok=True) + self.libs_dir = libs_dir_path + + self.info('{:20} {}'.format('Lib sources in:', self.libs_dir.absolute())) + + def _set_library_versions(self, build_platform): + defaults = self.UNIX_DEFAULT_LIB_VERSIONS + if build_platform == 'win32': + defaults = self.WINDOWS_DEFAULT_LIB_VERSIONS + + for version_attr, env_var in self.LIB_VERSION_ENV_VARS.items(): + setattr(self, version_attr, os.environ.get(env_var, defaults[version_attr])) + + def _prepare_windows_build(self, download_only=False): + if platform.machine() == 'ARM64': + suffix = 'win-arm64' + elif sys.maxsize > 2**32: + suffix = 'win64' + else: + suffix = 'win32' + + libs = [ + f'libxml2-{self.libxml2_version}.{suffix}.zip', + f'libxslt-{self.libxslt_version}.{suffix}.zip', + f'zlib-{self.zlib_version}.{suffix}.zip', + f'iconv-{self.libiconv_version}.{suffix}.zip', + f'openssl-{self.openssl_version}.{suffix}.zip', + f'xmlsec-{self.xmlsec1_version}.{suffix}.zip', + ] + + for libfile in libs: + url = urljoin(self.WINDOWS_LIBS_DOWNLOAD_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)) + + if download_only: + return + + for package in self.libs_dir.glob('*.zip'): + with zipfile.ZipFile(str(package)) as archive: + archive.extractall(path=str(self.build_libs_dir)) + + def _prepare_unix_build(self, build_platform, download_only=False): + archives = self._ensure_source_archives() + if download_only: + return + + 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) + + def _ensure_source_archives(self): + return [ + self._ensure_source( + name='OpenSSL', + glob='openssl*.tar.gz', + filename='openssl.tar.gz', + version=self.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.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.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.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.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.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: + if sys.version_info >= (3, 12): + tar.extractall(path=str(self.build_libs_dir), filter='data') + else: + 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': + if self.plat_name: + arch = self.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) + + +__all__ = ('CrossCompileInfo', 'LibXmlsecDependencyBuilder') 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..34d06ae6 --- /dev/null +++ b/build_support/static_build.py @@ -0,0 +1,115 @@ +import sys +from distutils.errors import DistutilsError + +from .lib_xmlsec_dependency_builder import CrossCompileInfo, LibXmlsecDependencyBuilder + + +class StaticBuildHelper: + def __init__(self, builder): + self.builder = builder + self.ext = builder.ext_map['xmlsec'] + self.info = builder.info + + def prepare(self, platform_name): + self.info(f'starting static build on {sys.platform}') + deps_builder = LibXmlsecDependencyBuilder( + platform_name=platform_name, + info=self.info, + plat_name=getattr(self.builder, 'plat_name', None), + ) + deps_builder.prepare() + + self.prefix_dir = deps_builder.prefix_dir + self.build_libs_dir = deps_builder.build_libs_dir + self.libs_dir = deps_builder.libs_dir + + self.builder.prefix_dir = self.prefix_dir + self.builder.build_libs_dir = self.build_libs_dir + self.builder.libs_dir = self.libs_dir + + for version_attr, value in deps_builder.versions.items(): + setattr(self.builder, version_attr, value) + + if platform_name == 'win32': + self._configure_windows_extension_for_static() + elif 'linux' in platform_name or 'darwin' in platform_name: + self._configure_unix_extension_for_static(platform_name) + else: + raise DistutilsError(f'Unsupported static build platform: {platform_name}') + + def _configure_windows_extension_for_static(self): + 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 _configure_unix_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 new file mode 100644 index 00000000..9b6324b7 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = python-xmlsec +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# 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) diff --git a/doc/source/api.rst b/doc/source/api.rst new file mode 100644 index 00000000..d142f3bd --- /dev/null +++ b/doc/source/api.rst @@ -0,0 +1,13 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 1 + + modules/xmlsec + modules/constants + modules/template + modules/tree + + +:ref:`contents` diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 00000000..900b79da --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import importlib.metadata +import urllib.request + +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 + +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] + +intersphinx_mapping = {'python': ('https://docs.python.org/3/', None)} + +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}' + +exclude_patterns: list[str] = [] +pygments_style = 'sphinx' +todo_include_todos = False + +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', + ) +] + +man_pages = [(master_doc, 'python-xmlsec', 'python-xmlsec Documentation', [author], 1)] + +texinfo_documents = [ + ( + master_doc, + 'python-xmlsec', + 'python-xmlsec Documentation', + author, + 'python-xmlsec', + 'One line description of project.', + 'Miscellaneous', + ) +] + +autodoc_member_order = 'groupwise' +autodoc_docstring_signature = True + + +rst_prolog = """ +.. role:: xml(code) + :language: xml +""" + +# LXML crossref'ing stuff: +# LXML doesn't have an intersphinx docs, +# so we link to lxml.etree._Element explicitly +lxml_element_cls_doc_uri = 'https://lxml.de/api/lxml.etree._Element-class.html' + + +def lxml_element_doc_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: Text) -> reference: + """Handle a missing reference only if it is a ``lxml.etree._Element`` ref. + + We handle only :class:`lxml.etree._Element` and :class:`~lxml.etree._Element` nodes. + """ + 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 + + +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 new file mode 100644 index 00000000..b5f9bb95 --- /dev/null +++ b/doc/source/examples.rst @@ -0,0 +1,35 @@ +Examples +======== + +Decrypt +------- + +.. literalinclude:: examples/decrypt.py + +Encrypt +------- + +.. literalinclude:: examples/encrypt.py + + +Sign +---- + +.. literalinclude:: examples/sign.py + +Sign-Binary +----------- + +.. literalinclude:: examples/sign_binary.py + + +Verify +------ + +.. literalinclude:: examples/verify.py + + +Verify-Binary +------------- + +.. 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/tests/examples/enc1-doc.xml b/doc/source/examples/enc1-doc.xml similarity index 92% rename from tests/examples/enc1-doc.xml rename to doc/source/examples/enc1-doc.xml index fd474859..39f8d761 100644 --- a/tests/examples/enc1-doc.xml +++ b/doc/source/examples/enc1-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/examples/enc1-res.xml b/doc/source/examples/enc1-res.xml similarity index 98% rename from tests/examples/enc1-res.xml rename to doc/source/examples/enc1-res.xml index 9499453b..ab0b1a6c 100644 --- a/tests/examples/enc1-res.xml +++ b/doc/source/examples/enc1-res.xml @@ -19,4 +19,4 @@ DY/U86tTpTn95NwHD10SLyrL6rpXdbEuoIQHhWLwV9uQxnJA/Pn1KZ+xXK/fePfP 2pb5Mxd0f+AW56Cs3MfQ9HJkUVeliSi1hVCNCVHTKeMyC2VL6lPhQ9+L01aSeTSY - \ No newline at end of file + 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/tests/examples/rsacert.pem b/doc/source/examples/rsacert.pem similarity index 96% rename from tests/examples/rsacert.pem rename to doc/source/examples/rsacert.pem index 02489a43..e8a68228 100644 --- a/tests/examples/rsacert.pem +++ b/doc/source/examples/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/examples/rsakey.pem b/doc/source/examples/rsakey.pem similarity index 100% rename from tests/examples/rsakey.pem rename to doc/source/examples/rsakey.pem diff --git a/tests/examples/rsapub.pem b/doc/source/examples/rsapub.pem similarity index 100% rename from tests/examples/rsapub.pem rename to doc/source/examples/rsapub.pem 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/tests/examples/sign1-res.xml b/doc/source/examples/sign1-res.xml similarity index 94% rename from tests/examples/sign1-res.xml rename to doc/source/examples/sign1-res.xml index 04d8fed0..f46ac1f4 100644 --- a/tests/examples/sign1-res.xml +++ b/doc/source/examples/sign1-res.xml @@ -1,6 +1,6 @@ - diff --git a/tests/examples/sign1-tmpl.xml b/doc/source/examples/sign1-tmpl.xml similarity index 97% rename from tests/examples/sign1-tmpl.xml rename to doc/source/examples/sign1-tmpl.xml index ac71a949..0a0cd442 100644 --- a/tests/examples/sign1-tmpl.xml +++ b/doc/source/examples/sign1-tmpl.xml @@ -1,6 +1,6 @@ - @@ -24,4 +24,3 @@ XML Security Library example: Simple signature template file for sign1 example. - 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 new file mode 100644 index 00000000..e08f47d9 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,28 @@ +.. 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. + +Welcome to python-xmlsec's documentation! +========================================= + +Python bindings for the XML Security Library. + +.. _contents: + +Table of contents +================= + +.. toctree:: + :maxdepth: 2 + + install + api + examples + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/install.rst b/doc/source/install.rst new file mode 100644 index 00000000..c892a3ea --- /dev/null +++ b/doc/source/install.rst @@ -0,0 +1,89 @@ +Installation +============ + +``xmlsec`` is available on PyPI: + +.. code-block:: bash + + pip install xmlsec + +Depending on your OS, you may need to install the required native +libraries first: + +Linux (Debian) +-------------- + +.. code-block:: bash + + apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl + +.. note:: There is no required version of LibXML2 for Ubuntu Precise, + so you need to download and install it manually: + + .. code-block:: bash + + wget http://xmlsoft.org/sources/libxml2-2.9.1.tar.gz + tar -xvf libxml2-2.9.1.tar.gz + cd libxml2-2.9.1 + ./configure && make && make install + + +Linux (CentOS) +-------------- + +.. code-block:: bash + + yum install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel + + +Linux (Fedora) +-------------- + +.. code-block:: bash + + dnf install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel + + +Mac +--- + +.. code-block:: bash + + xcode-select --install + brew upgrade + brew install libxml2 libxmlsec1 pkg-config + + +Alpine +------ + +.. code-block:: 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: + +#. Configure build environment, see `wiki.python.org `_ for more details. + +#. Install from source dist: + + .. code-block:: bash + + pip install xmlsec --no-binary=xmlsec diff --git a/doc/source/modules/constants.rst b/doc/source/modules/constants.rst new file mode 100644 index 00000000..3df6b50f --- /dev/null +++ b/doc/source/modules/constants.rst @@ -0,0 +1,495 @@ +``xmlsec.constants`` +-------------------- + +Various constants used by the library. + +EncryptionType +************** + +.. 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 +******* + +.. 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 +************* + +.. 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 +*********** + +.. 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 +********** + +.. 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 +***** + +.. 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 +********** + +.. 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 new file mode 100644 index 00000000..85551d2a --- /dev/null +++ b/doc/source/modules/template.rst @@ -0,0 +1,9 @@ +``xmlsec.template`` +------------------- + +.. automodule:: xmlsec.template + :members: + :undoc-members: + + +:ref:`contents` diff --git a/doc/source/modules/tree.rst b/doc/source/modules/tree.rst new file mode 100644 index 00000000..e70d6509 --- /dev/null +++ b/doc/source/modules/tree.rst @@ -0,0 +1,9 @@ +``xmlsec.tree`` +--------------- + +.. automodule:: xmlsec.tree + :members: + :undoc-members: + + +:ref:`contents` diff --git a/doc/source/modules/xmlsec.rst b/doc/source/modules/xmlsec.rst new file mode 100644 index 00000000..426bbaa4 --- /dev/null +++ b/doc/source/modules/xmlsec.rst @@ -0,0 +1,9 @@ +``xmlsec`` +---------- + +.. automodule:: xmlsec + :members: + :undoc-members: + + +:ref:`contents` 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..89ace626 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,189 @@ +[build-system] +requires = ["setuptools", "wheel", "setuptools_scm>=8", "pkgconfig>=1.5.1", "lxml>=3.8"] + +[project] +name = "xmlsec" +dynamic = ["version"] +description = "Python bindings for the XML Security Library" +readme = {file = "README.md", content-type = "text/markdown"} +requires-python = ">=3.9" +dependencies = ["lxml>=3.8"] +keywords = ["xmlsec"] +authors = [ + {name = "Bulat Gaifullin", email = "support@mehcode.com"} +] +maintainers = [ + {name = "Oleg Hoefling", email = "oleg.hoefling@gmail.com"}, + {name = "Amin Solhizadeh", email = "amin.solhizadeh@gmail.com"} +] +license = "MIT" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: C", + "Programming Language :: Python :: 3", + "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" +] + +[project.urls] +Documentation = "https://xmlsec.readthedocs.io" +Source = "https://github.com/xmlsec/python-xmlsec" +Changelog = "https://github.com/xmlsec/python-xmlsec/releases" + +# setuptools +[tool.setuptools] +zip-safe = false +packages = ["xmlsec"] +package-dir = {"" = "src"} + +[tool.setuptools.package-data] +xmlsec = ["py.typed", "*.pyi"] + +[tool.setuptools_scm] + +# mypy +[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 + +# TODO: Remove this override after adding full type annotations in build tooling modules. +[[tool.mypy.overrides]] +module = [ + "build_support.network", + "build_support.releases", + "build_support.lib_xmlsec_dependency_builder", + "build_libs_xmlsec" +] +disallow_untyped_calls = false +disallow_untyped_defs = false +disable_error_code = ["attr-defined"] + +# ruff +[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" + +# cibuildwheel +[tool.cibuildwheel] +build = [ + "cp39-*", + "cp310-*", + "cp311-*", + "cp312-*", + "cp313-*", + "cp314-*" +] +build-verbosity = 1 +environment = {PYXMLSEC_STATIC_DEPS="true"} +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" + +[tool.cibuildwheel.linux] +archs = ["x86_64", "aarch64", "riscv64"] +environment-pass = [ + "PYXMLSEC_STATIC_DEPS", + "GH_TOKEN" +] + +[tool.cibuildwheel.macos] +archs = ["x86_64", "arm64"] + +[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..70fe9703 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.14.4 diff --git a/requirements.txt b/requirements.txt index cd185c22..8221c374 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -pkgconfig -lxml >= 3.0 +lxml==6.0.2 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 28b996e9..00000000 --- a/setup.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[metadata] -description-file = README.rst - -[bdist_rpm] -release = 1 -build-requires = pkg-config xmlsec1-devel libxml2-devel xmlsec1-openssl-devel -group = Development/Libraries -requires = xmlsec1 xmlsec1-openssl diff --git a/setup.py b/setup.py index 0e8c6c73..946855f1 100644 --- a/setup.py +++ b/setup.py @@ -1,123 +1,15 @@ -from __future__ import print_function +from pathlib import Path -import glob -import os -from setuptools import setup -from setuptools import Extension -from setuptools.command import build_ext +from setuptools import Extension, setup +from build_support.build_ext import build_ext -__name__ = "xmlsec" -__version__ = "1.0.1" -__description__ = "Python bindings for the XML Security Library" +src_root = Path(__file__).parent / 'src' +sources = [str(path.relative_to(Path(__file__).parent)) for path in src_root.rglob('*.c')] +pyxmlsec = Extension('xmlsec', sources=sources) -def is_debug(): - return bool(os.getenv("PYXMLSEC_DEBUG")) - - -macroses = [("MODULE_NAME", __name__), ("MODULE_VERSION", __version__), ("MODULE_DOC", __description__)] -cflags = ["-g", "-std=c99", "-fno-strict-aliasing", "-Wno-error=declaration-after-statement", "-Werror=implicit-function-declaration"] - - -if is_debug(): - macroses.append(("PYXMLSEC_ENABLE_DEBUG", "1")) - cflags.extend(["-Wall", "-O0"]) -else: - cflags.extend(["-Os"]) - - -# values which requires escaping -require_escape = {"XMLSEC_CRYPTO"} - - -def add_to_list(target, up, need_to_escape=None): - if up is None: - return target - - value = set(target) - if need_to_escape: - for x in up: - if x[0] in need_to_escape: - value.add((x[0], '"{0}"'.format(x[1]))) - else: - value.add(x) - else: - value.update(up) - target[:] = list(value) - - -def find_sources(path): - return glob.glob(os.path.join(path, "*.c")) - - -def parse_requirements(filename, __cache={}): - try: - return __cache[filename] - except KeyError: - with open(filename) as stream: - result = __cache[filename] = [x for x in (y.strip() for y in stream) if x and not x.startswith('#')] - return result - - -class BuildExt(build_ext.build_ext): - def run(self): - self.patch_xmlsec() - build_ext.build_ext.run(self) - - def patch_xmlsec(self): - # at this moment all setup_requires are installed and we can safety import them - pkgconfig = __import__("pkgconfig") - lxml = __import__("lxml") - - ext = self.ext_map[__name__] - config = pkgconfig.parse("xmlsec1") - # need to escape XMLSEC_CRYPTO - # added build flags from pkg-config - for item in ('libraries', 'library_dirs', 'include_dirs'): - add_to_list(getattr(ext, item), config.get(item)) - - add_to_list(ext.define_macros, config.get('define_macros'), {"XMLSEC_CRYPTO"}) - add_to_list(ext.include_dirs, lxml.get_include()) - - -_xmlsec = Extension( - __name__, - sources=find_sources("./src"), - extra_compile_args=cflags, - libraries=[], - library_dirs=[], - include_dirs=[], - define_macros=macroses -) - setup( - name=__name__, - version=__version__, - description=__description__, - ext_modules=[_xmlsec], - cmdclass={'build_ext': BuildExt}, - setup_requires=parse_requirements('requirements.txt'), - install_requires=parse_requirements('requirements.txt'), - author="Bulat Gaifullin", - author_email='support@mehcode.com', - maintainer='Bulat Gaifullin', - maintainer_email='gaifullinbf@gmail.com', - url='https://github.com/mehcode/python-xmlsec', - download_url="https://github.com/mehcode/python-xmlsec/archive/v%s.tar.gz" % __version__, - license='MIT', - keywords=["xmlsec"], - classifiers=[ - 'Development Status :: 3 - Alpha', - '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.3', - 'Programming Language :: Python :: 3.4', - 'Topic :: Text Processing :: Markup :: XML' - ], + ext_modules=[pyxmlsec], + cmdclass={'build_ext': build_ext}, ) diff --git a/src/common.h b/src/common.h index 5f91bc2f..a6176551 100644 --- a/src/common.h +++ b/src/common.h @@ -13,11 +13,7 @@ #include "debug.h" #ifndef MODULE_NAME -#define MODULE_NAME "xmlsec" -#endif - -#ifndef MODULE_DOC -#define MODULE_DOC "The tiny python wrapper around xmlsec1 library." +#define MODULE_NAME xmlsec #endif #define JOIN(X,Y) DO_JOIN1(X,Y) diff --git a/src/constants.c b/src/constants.c index 4c890729..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 */ - PyType_GenericAlloc, /* tp_alloc */ - 0, /* tp_new */ - PyObject_Del /* 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 */ @@ -197,9 +230,9 @@ static PyTypeObject _PyXmlSec_KeyDataType = { 0, /* tp_descr_set */ 0, /* tp_dictoffset */ 0, /* tp_init */ - PyType_GenericAlloc, /* tp_alloc */ + 0, /* tp_alloc */ 0, /* tp_new */ - PyObject_Del /* tp_free */ + 0, /* tp_free */ }; PyTypeObject* PyXmlSec_KeyDataType = &_PyXmlSec_KeyDataType; @@ -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,8 @@ 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"); PYXMLSEC_CLOSE_NAMESPACE(nsCls); @@ -299,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 @@ -314,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 @@ -346,9 +372,14 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { PYXMLSEC_ADD_NODE_CONSTANT(NodeDataReference, "DATA_REFERENCE"); PYXMLSEC_ADD_NODE_CONSTANT(NodeKeyReference, "KEY_REFERENCE"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeReference, "REFERENCE"); PYXMLSEC_ADD_NODE_CONSTANT(NodeReferenceList, "REFERENCE_LIST"); PYXMLSEC_ADD_NODE_CONSTANT(NodeKeyInfo, "KEY_INFO"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeKeyName, "KEY_NAME"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeKeyValue, "KEY_VALUE"); + + PYXMLSEC_ADD_NODE_CONSTANT(NodeX509Data, "X509_DATA"); PYXMLSEC_CLOSE_NAMESPACE(nodeCls); #undef PYXMLSEC_ADD_NODE_CONSTANT @@ -387,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"); @@ -408,10 +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") -#if XMLSEC_VERSION_HEX > 306 +#endif +#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") @@ -442,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"); @@ -454,12 +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"); @@ -467,16 +512,26 @@ 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 PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformHmacSha1, "HMAC_SHA1"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformHmacSha224, "HMAC_SHA224"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformHmacSha256, "HMAC_SHA256"); 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 PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRsaSha1, "RSA_SHA1"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRsaSha224, "RSA_SHA224"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRsaSha256, "RSA_SHA256"); @@ -485,8 +540,13 @@ 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 PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformSha1, "SHA1"); PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformSha224, "SHA224"); @@ -494,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/debug.h b/src/debug.h index 0604a462..3f851bdc 100644 --- a/src/debug.h +++ b/src/debug.h @@ -15,9 +15,11 @@ #include #define PYXMLSEC_DEBUG(fmt) fprintf(stderr, "[%s:%d %s] " fmt "\n", __FILE__, __LINE__, __FUNCTION__) #define PYXMLSEC_DEBUGF(fmt, ...) fprintf(stderr, "[%s:%d %s] " fmt "\n", __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) +#define PYXMLSEC_DUMP(method, obj) method(obj, stderr) #else #define PYXMLSEC_DEBUG(...) #define PYXMLSEC_DEBUGF(...) +#define PYXMLSEC_DUMP(method, obj) #endif // PYXMLSEC_ENABLE_DEBUG #endif // __PYXMLSEC_DEBUG_H__ diff --git a/src/ds.c b/src/ds.c index 7a62715b..d0b4bdf9 100644 --- a/src/ds.c +++ b/src/ds.c @@ -34,27 +34,31 @@ static PyObject* PyXmlSec_SignatureContext__new__(PyTypeObject *type, PyObject * static int PyXmlSec_SignatureContext__init__(PyObject* self, PyObject* args, PyObject* kwargs) { static char *kwlist[] = { "manager", NULL}; - + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; PyXmlSec_KeysManager* manager = NULL; + PYXMLSEC_DEBUGF("%p: init sign context", self); - PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O&:__init__", kwlist, PyXmlSec_KeysManagerConvert, &manager)) { - return -1; + goto ON_FAIL; } - PYXMLSEC_DEBUGF("%p", manager); ctx->handle = xmlSecDSigCtxCreate(manager != NULL ? manager->handle : NULL); if (ctx->handle == NULL) { PyXmlSec_SetLastError("failed to create the digital signature context"); - return -1; + goto ON_FAIL; } - Py_XINCREF(manager); ctx->manager = manager; + PYXMLSEC_DEBUGF("%p: signMethod: %p", self, ctx->handle->signMethod); + PYXMLSEC_DEBUGF("%p: init sign context - ok, manager - %p", self, manager); return 0; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: init sign context - failed", self); + Py_XDECREF(manager); + return -1; } static void PyXmlSec_SignatureContext__del__(PyObject* self) { - PYXMLSEC_DEBUGF("%p: delete sign context", self); PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; + PYXMLSEC_DEBUGF("%p: delete sign context", self); if (ctx->handle != NULL) { xmlSecDSigCtxDestroy(ctx->handle); } @@ -65,25 +69,44 @@ static void PyXmlSec_SignatureContext__del__(PyObject* self) { static const char PyXmlSec_SignatureContextKey__doc__[] = "Signature key.\n"; static PyObject* PyXmlSec_SignatureContextKeyGet(PyObject* self, void* closure) { - PyXmlSec_Key* key = PyXmlSec_NewKey(); - key->handle = ((PyXmlSec_SignatureContext*)self)->handle->signKey; + PyXmlSec_SignatureContext* ctx = ((PyXmlSec_SignatureContext*)self); + PyXmlSec_Key* key; + + if (ctx->handle->signKey == NULL) { + Py_RETURN_NONE; + } + + key = PyXmlSec_NewKey(); + key->handle = ctx->handle->signKey; key->is_own = 0; return (PyObject*)key; } static int PyXmlSec_SignatureContextKeySet(PyObject* self, PyObject* value, void* closure) { + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; + 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; } - PyXmlSec_Key* key = (PyXmlSec_Key*)value; + key = (PyXmlSec_Key*)value; + if (key->handle == NULL) { PyErr_SetString(PyExc_TypeError, "empty key."); return -1; } - PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; if (ctx->handle->signKey != NULL) { xmlSecKeyDestroy(ctx->handle->signKey); } @@ -96,7 +119,15 @@ static int PyXmlSec_SignatureContextKeySet(PyObject* self, PyObject* value, void return 0; } -static const char PyXmlSec_SignatureContextRegisterId__doc__[] = "Register new id.\n"; +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" + ":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}; @@ -105,7 +136,8 @@ static PyObject* PyXmlSec_SignatureContextRegisterId(PyObject* self, PyObject* a const char* id_ns = NULL; xmlChar* name = NULL; - xmlAttrPtr attr = NULL; + xmlAttrPtr attr; + xmlAttrPtr tmpAttr; PYXMLSEC_DEBUGF("%p: register id - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&|sz:register_id", kwlist, @@ -126,7 +158,7 @@ static PyObject* PyXmlSec_SignatureContextRegisterId(PyObject* self, PyObject* a } name = xmlNodeListGetString(node->_c_node->doc, attr->children, 1); - xmlAttrPtr tmpAttr = xmlGetID(node->_c_node->doc, name); + tmpAttr = xmlGetID(node->_c_node->doc, name); if (tmpAttr != attr) { if (tmpAttr != NULL) { PyErr_SetString(PyXmlSec_Error, "duplicated id."); @@ -147,20 +179,26 @@ static PyObject* PyXmlSec_SignatureContextRegisterId(PyObject* self, PyObject* a return NULL; } -static const char PyXmlSec_SignatureContextSign__doc__[] = "Sign according to the signature template.\n"; +static const char PyXmlSec_SignatureContextSign__doc__[] = \ + "sign(node) -> None\n" + "Signs according to the signature template.\n\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}; + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; PyXmlSec_LxmlElementPtr node = NULL; + int rv; PYXMLSEC_DEBUGF("%p: sign - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:sign", kwlist, PyXmlSec_LxmlElementConverter, &node)) { goto ON_FAIL; } - int rv; Py_BEGIN_ALLOW_THREADS; - rv = xmlSecDSigCtxSign(((PyXmlSec_SignatureContext*)self)->handle, node->_c_node); + rv = xmlSecDSigCtxSign(ctx->handle, node->_c_node); + PYXMLSEC_DUMP(xmlSecDSigCtxDebugDump, ctx->handle); Py_END_ALLOW_THREADS; if (rv < 0) { PyXmlSec_SetLastError("failed to sign"); @@ -174,28 +212,35 @@ static PyObject* PyXmlSec_SignatureContextSign(PyObject* self, PyObject* args, P return NULL; } -static const char PyXmlSec_SignatureContextVerify__doc__[] = "Verify according to the signature template.\n"; +static const char PyXmlSec_SignatureContextVerify__doc__[] = \ + "verify(node) -> None\n" + "Verifies according to the signature template.\n\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}; + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; PyXmlSec_LxmlElementPtr node = NULL; + int rv; PYXMLSEC_DEBUGF("%p: verify - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:verify", kwlist, PyXmlSec_LxmlElementConverter, &node)) { goto ON_FAIL; } - xmlSecDSigCtxPtr handle = ((PyXmlSec_SignatureContext*)self)->handle; - int rv; Py_BEGIN_ALLOW_THREADS; - rv = xmlSecDSigCtxVerify(handle, node->_c_node); + rv = xmlSecDSigCtxVerify(ctx->handle, node->_c_node); + PYXMLSEC_DUMP(xmlSecDSigCtxDebugDump, ctx->handle); Py_END_ALLOW_THREADS; if (rv < 0) { PyXmlSec_SetLastError("failed to verify"); goto ON_FAIL; } - if (handle->status != xmlSecDSigStatusSucceeded) { + if (ctx->handle->status != xmlSecDSigStatusSucceeded) { PyErr_SetString(PyXmlSec_VerificationError, "Signature is invalid."); goto ON_FAIL; } @@ -207,63 +252,72 @@ static PyObject* PyXmlSec_SignatureContextVerify(PyObject* self, PyObject* args, } // common helper for operations binary_verify and binary_sign -static int PyXmlSec_ProcessSignBinary(xmlSecDSigCtxPtr ctx, const xmlSecByte* data, xmlSecSize data_size, xmlSecTransformId method) { +static int PyXmlSec_ProcessSignBinary(PyXmlSec_SignatureContext* ctx, const xmlSecByte* data, xmlSecSize data_size, xmlSecTransformId method) { + int rv; + if (!(method->usage & xmlSecTransformUsageSignatureMethod)) { PyErr_SetString(PyXmlSec_Error, "incompatible signature method"); return -1; } - if (ctx->signKey == NULL) { + if (ctx->handle->signKey == NULL) { PyErr_SetString(PyXmlSec_Error, "Sign key is not specified."); + return -1; } - if (ctx->signMethod != NULL) { + if (ctx->handle->signMethod != NULL) { + PYXMLSEC_DEBUGF("%p: signMethod: %p", ctx, ctx->handle->signMethod); PyErr_SetString(PyXmlSec_Error, "Signature context already used; it is designed for one use only."); return -1; } - ctx->signMethod = xmlSecTransformCtxCreateAndAppend(&(ctx->transformCtx), method); - if (ctx->signMethod == NULL) { + ctx->handle->signMethod = xmlSecTransformCtxCreateAndAppend(&(ctx->handle->transformCtx), method); + if (ctx->handle->signMethod == NULL) { PyXmlSec_SetLastError("could not create signature transform."); return -1; } - int rv; - - ctx->signMethod->operation = ctx->operation; - xmlSecTransformSetKeyReq(ctx->signMethod, &(ctx->keyInfoReadCtx.keyReq)); - rv = xmlSecKeyMatch(ctx->signKey, NULL, &(ctx->keyInfoReadCtx.keyReq)); + ctx->handle->signMethod->operation = ctx->handle->operation; + xmlSecTransformSetKeyReq(ctx->handle->signMethod, &(ctx->handle->keyInfoReadCtx.keyReq)); + rv = xmlSecKeyMatch(ctx->handle->signKey, NULL, &(ctx->handle->keyInfoReadCtx.keyReq)); if (rv != 1) { PyXmlSec_SetLastError("inappropriate key type."); return -1; } - rv = xmlSecTransformSetKey(ctx->signMethod, ctx->signKey); + rv = xmlSecTransformSetKey(ctx->handle->signMethod, ctx->handle->signKey); if (rv < 0) { PyXmlSec_SetLastError("cannot set key."); return -1; } - ctx->transformCtx.result = NULL; - ctx->transformCtx.status = xmlSecTransformStatusNone; + ctx->handle->transformCtx.result = NULL; + ctx->handle->transformCtx.status = xmlSecTransformStatusNone; Py_BEGIN_ALLOW_THREADS; - rv = xmlSecTransformCtxBinaryExecute(&(ctx->transformCtx), data, data_size); + rv = xmlSecTransformCtxBinaryExecute(&(ctx->handle->transformCtx), data, data_size); Py_END_ALLOW_THREADS; if (rv < 0) { PyXmlSec_SetLastError("failed to transform."); return -1; } - ctx->result = ctx->transformCtx.result; + ctx->handle->result = ctx->handle->transformCtx.result; return 0; } static const char PyXmlSec_SignatureContextSignBinary__doc__[] = \ - "Sign binary data *data* with *algorithm* and return the signature.\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" + ":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; PyXmlSec_Transform* transform = NULL; const char* data = NULL; Py_ssize_t data_size = 0; @@ -275,8 +329,7 @@ static PyObject* PyXmlSec_SignatureContextSignBinary(PyObject* self, PyObject* a goto ON_FAIL; } - xmlSecDSigCtxPtr ctx = ((PyXmlSec_SignatureContext*)self)->handle; - ctx->operation = xmlSecTransformOperationSign; + ctx->handle->operation = xmlSecTransformOperationSign; if (PyXmlSec_ProcessSignBinary(ctx, (const xmlSecByte*)data, (xmlSecSize)data_size, transform->id) != 0) { goto ON_FAIL; @@ -284,7 +337,8 @@ static PyObject* PyXmlSec_SignatureContextSignBinary(PyObject* self, PyObject* a PYXMLSEC_DEBUGF("%p: sign_binary - ok", self); return PyBytes_FromStringAndSize( - (const char*)xmlSecBufferGetData(ctx->result), (Py_ssize_t)xmlSecBufferGetSize(ctx->result) + (const char*)xmlSecBufferGetData(ctx->handle->result), + (Py_ssize_t)xmlSecBufferGetSize(ctx->handle->result) ); ON_FAIL: PYXMLSEC_DEBUGF("%p: sign_binary - fail", self); @@ -292,15 +346,26 @@ static PyObject* PyXmlSec_SignatureContextSignBinary(PyObject* self, PyObject* a } static const char PyXmlSec_SignatureContextVerifyBinary__doc__[] = \ - "Sign binary data *data* with *algorithm* and return the signature.\n"; + "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" + ":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}; + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; PyXmlSec_Transform* transform = NULL; const char* data = NULL; Py_ssize_t data_size = 0; const char* sign = NULL; Py_ssize_t sign_size = 0; + int rv; PYXMLSEC_DEBUGF("%p: verify binary - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s#O!s#:verify_binary", kwlist, @@ -309,15 +374,13 @@ static PyObject* PyXmlSec_SignatureContextVerifyBinary(PyObject* self, PyObject* goto ON_FAIL; } - xmlSecDSigCtxPtr ctx = ((PyXmlSec_SignatureContext*)self)->handle; - ctx->operation = xmlSecTransformOperationVerify; + ctx->handle->operation = xmlSecTransformOperationVerify; if (PyXmlSec_ProcessSignBinary(ctx, (const xmlSecByte*)data, (xmlSecSize)data_size, transform->id) != 0) { goto ON_FAIL; } - int rv; Py_BEGIN_ALLOW_THREADS; - rv = xmlSecTransformVerify(ctx->signMethod, (const xmlSecByte*)sign, (xmlSecSize)sign_size, &(ctx->transformCtx)); + rv = xmlSecTransformVerify(ctx->handle->signMethod, (const xmlSecByte*)sign, (xmlSecSize)sign_size, &(ctx->handle->transformCtx)); Py_END_ALLOW_THREADS; if (rv < 0) { @@ -325,7 +388,7 @@ static PyObject* PyXmlSec_SignatureContextVerifyBinary(PyObject* self, PyObject* goto ON_FAIL; } - if (ctx->signMethod->status != xmlSecTransformStatusOk) { + if (ctx->handle->signMethod->status != xmlSecTransformStatusOk) { PyXmlSec_SetLastError2(PyXmlSec_VerificationError, "Signature is invalid."); goto ON_FAIL; } @@ -338,13 +401,18 @@ static PyObject* PyXmlSec_SignatureContextVerifyBinary(PyObject* self, PyObject* } static const char PyXmlSec_SignatureContextEnableReferenceTransform__doc__[] = \ - "Enables use of *t* as reference transform.\n"\ - "Note: by default, all transforms are enabled. The first call of\n"\ - "`enable_reference_transform` will switch to explicitly enabled transforms.\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}; + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; PyXmlSec_Transform* transform = NULL; + int rv; PYXMLSEC_DEBUGF("%p: enable_reference_transform - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!:enable_reference_transform", kwlist, PyXmlSec_TransformType, &transform)) @@ -352,9 +420,8 @@ static PyObject* PyXmlSec_SignatureContextEnableReferenceTransform(PyObject* sel goto ON_FAIL; } - int rv; Py_BEGIN_ALLOW_THREADS; - rv = xmlSecDSigCtxEnableReferenceTransform(((PyXmlSec_SignatureContext*)self)->handle, transform->id); + rv = xmlSecDSigCtxEnableReferenceTransform(ctx->handle, transform->id); Py_END_ALLOW_THREADS; if (rv < 0) { @@ -370,22 +437,26 @@ static PyObject* PyXmlSec_SignatureContextEnableReferenceTransform(PyObject* sel } static const char PyXmlSec_SignatureContextEnableSignatureTransform__doc__[] = \ - "Enables use of *t* as signature transform.\n"\ - "Note: by default, all transforms are enabled. The first call of\n"\ - "`enable_signature_transform` will switch to explicitly enabled transforms.\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}; + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; PyXmlSec_Transform* transform = NULL; + int rv; PYXMLSEC_DEBUGF("%p: enable_signature_transform - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!:enable_signature_transform", kwlist, PyXmlSec_TransformType, &transform)) { goto ON_FAIL; } - int rv; Py_BEGIN_ALLOW_THREADS; - rv = xmlSecDSigCtxEnableSignatureTransform(((PyXmlSec_SignatureContext*)self)->handle, transform->id); + rv = xmlSecDSigCtxEnableSignatureTransform(ctx->handle, transform->id); Py_END_ALLOW_THREADS; if (rv < 0) { @@ -401,13 +472,18 @@ static PyObject* PyXmlSec_SignatureContextEnableSignatureTransform(PyObject* sel } static const char PyXmlSec_SignatureContextSetEnabledKeyData__doc__[] = \ - "Adds selected *KeyData* to the list of enabled key data 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}; + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; PyObject* keydata_list = NULL; PyObject* iter = NULL; PyObject* item = NULL; + xmlSecPtrListPtr enabled_list; PYXMLSEC_DEBUGF("%p: set_enabled_key_data - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O:set_enabled_key_data", kwlist, &keydata_list)) { @@ -415,7 +491,7 @@ static PyObject* PyXmlSec_SignatureContextSetEnabledKeyData(PyObject* self, PyOb } if ((iter = PyObject_GetIter(keydata_list)) == NULL) goto ON_FAIL; - xmlSecPtrListPtr enabled_list = &(((PyXmlSec_SignatureContext*)self)->handle->keyInfoReadCtx.enabledKeyData); + enabled_list = &(ctx->handle->keyInfoReadCtx.enabledKeyData); xmlSecPtrListEmpty(enabled_list); while ((item = PyIter_Next(iter)) != NULL) { @@ -541,9 +617,9 @@ static PyTypeObject _PyXmlSec_SignatureContextType = { 0, /* tp_descr_set */ 0, /* tp_dictoffset */ PyXmlSec_SignatureContext__init__, /* tp_init */ - PyType_GenericAlloc, /* tp_alloc */ + 0, /* tp_alloc */ PyXmlSec_SignatureContext__new__, /* tp_new */ - PyObject_Del /* tp_free */ + 0, /* tp_free */ }; PyTypeObject* PyXmlSec_SignatureContextType = &_PyXmlSec_SignatureContextType; diff --git a/src/enc.c b/src/enc.c index 3c8fc2ec..42195dd3 100644 --- a/src/enc.c +++ b/src/enc.c @@ -15,6 +15,12 @@ #include "lxml.h" #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 @@ -24,7 +30,7 @@ typedef struct { static PyObject* PyXmlSec_EncryptionContext__new__(PyTypeObject *type, PyObject *args, PyObject *kwargs) { PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)PyType_GenericNew(type, args, kwargs); - PYXMLSEC_DEBUGF("%p: new sign context", ctx); + PYXMLSEC_DEBUGF("%p: new enc context", ctx); if (ctx != NULL) { ctx->handle = NULL; ctx->manager = NULL; @@ -37,25 +43,37 @@ static int PyXmlSec_EncryptionContext__init__(PyObject* self, PyObject* args, Py PyXmlSec_KeysManager* manager = NULL; PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; - PYXMLSEC_DEBUGF("%p: init sign context", self); + + PYXMLSEC_DEBUGF("%p: init enc context", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O&:__init__", kwlist, PyXmlSec_KeysManagerConvert, &manager)) { - return -1; + goto ON_FAIL; } - - PYXMLSEC_DEBUGF("%p", manager); ctx->handle = xmlSecEncCtxCreate(manager != NULL ? manager->handle : NULL); if (ctx->handle == NULL) { - PyXmlSec_SetLastError("failed to create the digital signature context"); - return -1; + PyXmlSec_SetLastError("failed to create the encryption context"); + goto ON_FAIL; } - Py_XINCREF(manager); 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); + Py_XDECREF(manager); + return -1; } static void PyXmlSec_EncryptionContext__del__(PyObject* self) { - PYXMLSEC_DEBUGF("%p: delete sign context", self); PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; + + PYXMLSEC_DEBUGF("%p: delete enc context", self); + if (ctx->handle != NULL) { xmlSecEncCtxDestroy(ctx->handle); } @@ -66,31 +84,49 @@ static void PyXmlSec_EncryptionContext__del__(PyObject* self) { static const char PyXmlSec_EncryptionContextKey__doc__[] = "Encryption key.\n"; static PyObject* PyXmlSec_EncryptionContextKeyGet(PyObject* self, void* closure) { - PyXmlSec_Key* key = PyXmlSec_NewKey(); - key->handle = ((PyXmlSec_EncryptionContext*)self)->handle->encKey; + PyXmlSec_EncryptionContext* ctx = ((PyXmlSec_EncryptionContext*)self); + PyXmlSec_Key* key; + + if (ctx->handle->encKey == NULL) { + Py_RETURN_NONE; + } + + key = PyXmlSec_NewKey(); + key->handle = ctx->handle->encKey; key->is_own = 0; return (PyObject*)key; } static int PyXmlSec_EncryptionContextKeySet(PyObject* self, PyObject* value, void* closure) { + PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; + 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; } - xmlSecKeyPtr keyHandle = ((PyXmlSec_Key*)value)->handle; - if (keyHandle == NULL) { + key = (PyXmlSec_Key*)value; + if (key->handle == NULL) { PyErr_SetString(PyExc_TypeError, "empty key."); return -1; } - PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; if (ctx->handle->encKey != NULL) { xmlSecKeyDestroy(ctx->handle->encKey); } - ctx->handle->encKey = xmlSecKeyDuplicate(keyHandle); + ctx->handle->encKey = xmlSecKeyDuplicate(key->handle); if (ctx->handle->encKey == NULL) { PyXmlSec_SetLastError("failed to duplicate key"); return -1; @@ -98,16 +134,39 @@ static int PyXmlSec_EncryptionContextKeySet(PyObject* self, PyObject* value, voi return 0; } +static const char PyXmlSec_EncryptionContextReset__doc__[] = \ + "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; + + PYXMLSEC_DEBUGF("%p: reset context - start", self); + Py_BEGIN_ALLOW_THREADS; + xmlSecEncCtxReset(ctx->handle); + PYXMLSEC_DUMP(xmlSecEncCtxDebugDump, ctx->handle); + Py_END_ALLOW_THREADS; + PYXMLSEC_DEBUGF("%p: reset context - ok", self); + Py_RETURN_NONE; +} + static const char PyXmlSec_EncryptionContextEncryptBinary__doc__[] = \ - "Encrypts binary *data* according to `EncryptedData` template *template*\n"\ - "returns the resulting `EncryptedData` subtree.\n" \ - "Note: *template* is modified in place.\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" + ":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}; + PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; PyXmlSec_LxmlElementPtr template = NULL; const char* data = NULL; Py_ssize_t data_size = 0; + int rv; PYXMLSEC_DEBUGF("%p: encrypt_binary - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&s#:encrypt_binary", kwlist, @@ -115,10 +174,10 @@ static PyObject* PyXmlSec_EncryptionContextEncryptBinary(PyObject* self, PyObjec { goto ON_FAIL; } - xmlSecEncCtxPtr ctx = ((PyXmlSec_EncryptionContext*)self)->handle; - int rv; + Py_BEGIN_ALLOW_THREADS; - rv = xmlSecEncCtxBinaryEncrypt(ctx, template->_c_node, (const xmlSecByte*)data, (xmlSecSize)data_size); + rv = xmlSecEncCtxBinaryEncrypt(ctx->handle, template->_c_node, (const xmlSecByte*)data, (xmlSecSize)data_size); + PYXMLSEC_DUMP(xmlSecEncCtxDebugDump, ctx->handle); Py_END_ALLOW_THREADS; if (rv < 0) { @@ -136,35 +195,47 @@ 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; + while (n != NULL) { + PYXMLSEC_DEBUGF("clear replaced node %p", n); nn = n->next; // if n has references, it will not be deleted - Py_XDECREF(PyXmlSec_elementFactory(doc, n)); + elem = (PyXmlSec_LxmlElementPtr)PyXmlSec_elementFactory(doc, n); + if (NULL == elem) + xmlFreeNode(n); + else + Py_DECREF(elem); n = nn; } ctx->replacedNodeList = NULL; } static const char PyXmlSec_EncryptionContextEncryptXml__doc__[] = \ - "Encrpyts *node* using *template*.\n" \ - "Returns the resulting `EncryptedData` element.\n\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 containing *node* in a way that\n"\ - "`lxml` references to or into this tree may see a surprising state.\n"\ - "You should no longer rely on them. Especially, you should use\n"\ - "`getroottree()` on the result to obtain the encrypted result tree.\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}; + PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; PyXmlSec_LxmlElementPtr template = NULL; PyXmlSec_LxmlElementPtr node = NULL; xmlNodePtr xnew_node = NULL; xmlChar* tmpType = NULL; + int rv = 0; PYXMLSEC_DEBUGF("%p: encrypt_xml - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O&:encrypt_xml", kwlist, @@ -174,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; } @@ -182,9 +253,7 @@ static PyObject* PyXmlSec_EncryptionContextEncryptXml(PyObject* self, PyObject* // at `node._c_node` or its children by an extended subtree rooted at "c_node". // We set `XMLSEC_ENC_RETURN_REPLACED_NODE` to prevent deallocation // of the replaced node. This is important as `node` is still referencing it - xmlSecEncCtxPtr ctx = ((PyXmlSec_EncryptionContext*)self)->handle; - ctx->flags = XMLSEC_ENC_RETURN_REPLACED_NODE; - int rv = 0; + ctx->handle->flags = XMLSEC_ENC_RETURN_REPLACED_NODE; // try to do all actions whithin single python-free section // rv has the following codes, 1 - failed to copy node, -1 - op failed, 0 - success @@ -197,16 +266,20 @@ static PyObject* PyXmlSec_EncryptionContextEncryptXml(PyObject* self, PyObject* rv = 1; } } - if (rv == 0 && xmlSecEncCtxXmlEncrypt(ctx, xnew_node != NULL ? xnew_node: template->_c_node, node->_c_node) < 0) { + if (rv == 0 && xmlSecEncCtxXmlEncrypt(ctx->handle, xnew_node != NULL ? xnew_node: template->_c_node, node->_c_node) < 0) { rv = -1; if (xnew_node != NULL) { xmlFreeNode(xnew_node); xnew_node = NULL; } } + PYXMLSEC_DUMP(xmlSecEncCtxDebugDump, ctx->handle); Py_END_ALLOW_THREADS; - PyXmlSec_ClearReplacedNodes(ctx, node->_doc); + PyXmlSec_ClearReplacedNodes(ctx->handle, node->_doc); + if (NULL != PyErr_Occurred()) { + goto ON_FAIL; + } if (rv != 0) { if (rv > 0) { @@ -218,6 +291,7 @@ static PyObject* PyXmlSec_EncryptionContextEncryptXml(PyObject* self, PyObject* } xmlFree(tmpType); + PYXMLSEC_DEBUGF("%p: encrypt_xml - ok", self); return (PyObject*)PyXmlSec_elementFactory(node->_doc, xnew_node != NULL ? xnew_node : template->_c_node); ON_FAIL: @@ -227,22 +301,31 @@ static PyObject* PyXmlSec_EncryptionContextEncryptXml(PyObject* self, PyObject* } static const char PyXmlSec_EncryptionContextEncryptUri__doc__[] = \ - "Encrypts binary data obtained from *uri* according to *template*.\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" + ":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}; + PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; PyXmlSec_LxmlElementPtr template = NULL; const char* uri = NULL; + int rv; PYXMLSEC_DEBUGF("%p: encrypt_uri - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&s:encrypt_uri", kwlist, PyXmlSec_LxmlElementConverter, &template, &uri)) { goto ON_FAIL; } - xmlSecEncCtxPtr ctx = ((PyXmlSec_EncryptionContext*)self)->handle; - int rv; Py_BEGIN_ALLOW_THREADS; - rv = xmlSecEncCtxUriEncrypt(ctx, template->_c_node, (const xmlSecByte*)uri); + rv = xmlSecEncCtxUriEncrypt(ctx->handle, template->_c_node, (const xmlSecByte*)uri); + PYXMLSEC_DUMP(xmlSecEncCtxDebugDump, ctx->handle); Py_END_ALLOW_THREADS; if (rv < 0) { @@ -258,29 +341,39 @@ static PyObject* PyXmlSec_EncryptionContextEncryptUri(PyObject* self, PyObject* } static const char PyXmlSec_EncryptionContextDecrypt__doc__[] = \ - "Decrypts *node* (an `EncryptedData` 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, `lxml` references to or into this tree may see a surprising state.\n"\ - "You should no longer rely on them. Especially, you should use `getroottree()` on the result\n"\ - "to obtain the decrypted result tree.\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}; + PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; PyXmlSec_LxmlElementPtr node = NULL; PyObject* node_num = NULL; PyObject* parent = NULL; + PyObject* tmp; + xmlNodePtr root; + xmlNodePtr xparent; + int rv; + xmlChar* ttype; + int notContent; PYXMLSEC_DEBUGF("%p: decrypt - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:decrypt", kwlist, PyXmlSec_LxmlElementConverter, &node)) { goto ON_FAIL; } - xmlNodePtr xparent = node->_c_node->parent; - if (xparent != NULL && !_isElement(xparent)) { + xparent = node->_c_node->parent; + if (xparent != NULL && !PyXmlSec_IsElement(xparent)) { xparent = NULL; } @@ -292,51 +385,52 @@ static PyObject* PyXmlSec_EncryptionContextDecrypt(PyObject* self, PyObject* arg } // get index of node node_num = PyObject_CallMethod(parent, "index", "O", node); - PYXMLSEC_DEBUGF("%p, %p", parent, node_num); + PYXMLSEC_DEBUGF("parent: %p, %p", parent, node_num); } - xmlSecEncCtxPtr ctx = ((PyXmlSec_EncryptionContext*)self)->handle; - ctx->flags = XMLSEC_ENC_RETURN_REPLACED_NODE; - int rv; - Py_BEGIN_ALLOW_THREADS; - rv = xmlSecEncCtxDecrypt(ctx, node->_c_node); + ctx->handle->flags = XMLSEC_ENC_RETURN_REPLACED_NODE; + ctx->handle->mode = xmlSecCheckNodeName(node->_c_node, xmlSecNodeEncryptedKey, xmlSecEncNs) ? xmlEncCtxModeEncryptedKey : xmlEncCtxModeEncryptedData; + PYXMLSEC_DEBUGF("mode: %d", ctx->handle->mode); + rv = xmlSecEncCtxDecrypt(ctx->handle, node->_c_node); + PYXMLSEC_DUMP(xmlSecEncCtxDebugDump, ctx->handle); Py_END_ALLOW_THREADS; - PyXmlSec_ClearReplacedNodes(ctx, node->_doc); + PyXmlSec_ClearReplacedNodes(ctx->handle, node->_doc); if (rv < 0) { PyXmlSec_SetLastError("failed to decrypt"); goto ON_FAIL; } - if (!ctx->resultReplaced) { + if (!ctx->handle->resultReplaced) { Py_XDECREF(node_num); Py_XDECREF(parent); - PYXMLSEC_DEBUGF("%p: decrypt - ok", self); + PYXMLSEC_DEBUGF("%p: binary.decrypt - ok", self); return PyBytes_FromStringAndSize( - (const char*)xmlSecBufferGetData(ctx->result), (Py_ssize_t)xmlSecBufferGetSize(ctx->result) + (const char*)xmlSecBufferGetData(ctx->handle->result), + (Py_ssize_t)xmlSecBufferGetSize(ctx->handle->result) ); } if (xparent != NULL) { - xmlChar* ttype = xmlGetProp(node->_c_node, XSTR("Type")); - int notContent = (ttype == NULL || !xmlStrEqual(ttype, xmlSecTypeEncContent)); + ttype = xmlGetProp(node->_c_node, XSTR("Type")); + notContent = (ttype == NULL || !xmlStrEqual(ttype, xmlSecTypeEncContent)); xmlFree(ttype); if (notContent) { - PyObject* tmp = PyObject_GetItem(parent, node_num); + tmp = PyObject_GetItem(parent, node_num); if (tmp == NULL) goto ON_FAIL; Py_DECREF(parent); parent = tmp; } Py_DECREF(node_num); - PYXMLSEC_DEBUGF("%p: decrypt - ok", self); + PYXMLSEC_DEBUGF("%p: parent.decrypt - ok", self); return parent; } // root has been replaced - xmlNodePtr root = xmlDocGetRootElement(node->_doc->_c_doc); + root = xmlDocGetRootElement(node->_doc->_c_doc); if (root == NULL) { PyErr_SetString(PyXmlSec_Error, "decryption resulted in a non well formed document"); goto ON_FAIL; @@ -367,6 +461,12 @@ static PyGetSetDef PyXmlSec_EncryptionContextGetSet[] = { }; static PyMethodDef PyXmlSec_EncryptionContextMethods[] = { + { + "reset", + (PyCFunction)PyXmlSec_EncryptionContextReset, + METH_NOARGS, + PyXmlSec_EncryptionContextReset__doc__, + }, { "encrypt_binary", (PyCFunction)PyXmlSec_EncryptionContextEncryptBinary, @@ -431,9 +531,9 @@ static PyTypeObject _PyXmlSec_EncryptionContextType = { 0, /* tp_descr_set */ 0, /* tp_dictoffset */ PyXmlSec_EncryptionContext__init__, /* tp_init */ - PyType_GenericAlloc, /* tp_alloc */ + 0, /* tp_alloc */ PyXmlSec_EncryptionContext__new__, /* tp_new */ - PyObject_Del /* tp_free */ + 0 /* tp_free */ }; PyTypeObject* PyXmlSec_EncryptionContextType = &_PyXmlSec_EncryptionContextType; diff --git a/src/exception.c b/src/exception.c index 3de0040d..ac0e44ee 100644 --- a/src/exception.c +++ b/src/exception.c @@ -16,12 +16,20 @@ #include +#include + // default error class 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; typedef struct { const xmlChar* file; @@ -64,16 +72,29 @@ void PyXmlSec_ErrorHolderFree(PyXmlSec_ErrorHolder* h) { // saves new error in TLS and returns previous 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 - PyXmlSec_ErrorHolder* v = (PyXmlSec_ErrorHolder*)PyThread_get_key_value(PyXmlSec_LastErrorKey); + #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); - int r = PyThread_set_key_value(PyXmlSec_LastErrorKey, (void*)e); + r = PyThread_set_key_value(PyXmlSec_LastErrorKey, (void*)e); + #endif PYXMLSEC_DEBUGF("set_key_value returns %d", r); return v; } @@ -83,19 +104,40 @@ static void PyXmlSec_ErrorCallback(const char* file, int line, const char* func, // TODO do not allocate error object each time. PyXmlSec_ErrorHolderFree(PyXmlSec_ExchangeLastError(PyXmlSec_ErrorHolderCreate(file, line, func, object, subject, reason, msg))); - // also call default callback - xmlSecErrorsDefaultCallback(file, line, func, object, subject, reason, msg); + if (PyXmlSec_PrintErrorMessage) { + const char* error_msg = NULL; + xmlSecSize i; + for (i = 0; (i < XMLSEC_ERRORS_MAX_NUMBER) && (xmlSecErrorsGetMsg(i) != NULL); ++i) { + if(xmlSecErrorsGetCode(i) == reason) { + error_msg = xmlSecErrorsGetMsg(i); + break; + } + } + + fprintf(stderr, + "func=%s:file=%s:line=%d:obj=%s:subj=%s:error=%d:%s:%s\n", + (func != NULL) ? func : "unknown", + (file != NULL) ? file : "unknown", + line, + (object != NULL) ? object : "unknown", + (subject != NULL) ? subject : "unknown", + reason, + (error_msg != NULL) ? error_msg : "", + (msg != NULL) ? msg : ""); + } } // pops the last error which was occurred in current thread // the gil should be acquired static PyObject* PyXmlSec_GetLastError(PyObject* type, const char* msg) { PyXmlSec_ErrorHolder* h = PyXmlSec_ExchangeLastError(NULL); + PyObject* exc; + if (h == NULL) { return NULL; } - PyObject* exc = PyObject_CallFunction(type, "is", h->reason, msg); + exc = PyObject_CallFunction(type, "is", h->reason, msg); if (exc == NULL) goto ON_FAIL; PyXmlSec_SetLongAttr(exc, "code", h->reason); @@ -122,6 +164,7 @@ void PyXmlSec_SetLastError2(PyObject* type, const char* msg) { } } PyErr_SetObject(type, last); + Py_DECREF(last); } void PyXmlSec_SetLastError(const char* msg) { @@ -132,6 +175,20 @@ void PyXmlSec_ClearError(void) { PyXmlSec_ErrorHolderFree(PyXmlSec_ExchangeLastError(NULL)); } +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; @@ -151,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 a94216c6..687cd778 100644 --- a/src/exception.h +++ b/src/exception.h @@ -22,4 +22,8 @@ void PyXmlSec_SetLastError2(PyObject* type, const char* msg); 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 dc88f29f..5ff04aae 100644 --- a/src/keys.c +++ b/src/keys.c @@ -27,8 +27,8 @@ static PyObject* PyXmlSec_Key__new__(PyTypeObject *type, PyObject *args, PyObjec } static void PyXmlSec_Key__del__(PyObject* self) { - PYXMLSEC_DEBUGF("%p: delete key", self); PyXmlSec_Key* key = (PyXmlSec_Key*)self; + PYXMLSEC_DEBUGF("%p: delete key", self); if (key->is_own) { PYXMLSEC_DEBUGF("%p: delete handle - %p", self, key->handle); xmlSecKeyDestroy(key->handle); @@ -41,10 +41,12 @@ static PyXmlSec_Key* PyXmlSec_NewKey1(PyTypeObject* type) { } static PyObject* PyXmlSec_Key__copy__(PyObject* self) { + xmlSecKeyPtr handle = ((PyXmlSec_Key*)self)->handle; + PyXmlSec_Key* key2; + PYXMLSEC_DEBUGF("%p: copy key", self); - xmlSecKeyPtr handle = ((PyXmlSec_Key*)self)->handle; - PyXmlSec_Key* key2 = PyXmlSec_NewKey1(Py_TYPE(self)); + key2 = PyXmlSec_NewKey1(Py_TYPE(self)); if (handle == NULL || key2 == NULL) { PYXMLSEC_DEBUGF("%p: null key", self); @@ -65,7 +67,17 @@ static PyObject* PyXmlSec_Key__copy__(PyObject* self) { return (PyObject*)key2; } -static const char PyXmlSec_KeyFromMemory__doc__[] = "Load PKI key from memory.\n"; +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" + ":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}; @@ -104,9 +116,20 @@ static PyObject* PyXmlSec_KeyFromMemory(PyObject* self, PyObject* args, PyObject return NULL; } -static const char PyXmlSec_KeyFromFile__doc__[] = "Load PKI key from a file.\n"; +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" + ":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[] = { "data", "format", "password", NULL}; + static char *kwlist[] = { "file", "format", "password", NULL}; PyObject* file = NULL; const char* password = NULL; @@ -119,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; } @@ -140,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; @@ -162,9 +190,64 @@ static PyObject* PyXmlSec_KeyFromFile(PyObject* self, PyObject* args, PyObject* return NULL; } -static const char PyXmlSec_KeyGenerate__doc__[] = "Generate key of kind *data* with *size* and *type*.\n"; +static const char PyXmlSec_KeyFromEngine__doc__[] = \ + "from_engine(engine_and_key_id) -> xmlsec.Key\n" + "Loads PKI key from an engine.\n\n" + ":param engine_and_key_id: engine and key id, i.e. 'pkcs11;pkcs11:token=XmlsecToken;object=XmlsecKey;pin-value=password'\n" + ":type engine_and_key_id: :class:`str`, " + ":return: pointer to newly created key\n" + ":rtype: :class:`~xmlsec.Key`"; +static PyObject* PyXmlSec_KeyFromEngine(PyObject* self, PyObject* args, PyObject* kwargs) { + static char *kwlist[] = {"engine_and_key_id", NULL}; + + const char* engine_and_key_id = NULL; + PyXmlSec_Key* key = NULL; + + PYXMLSEC_DEBUG("load key from engine - start"); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s:from_engine", kwlist, &engine_and_key_id)) { + goto ON_FAIL; + } + + if ((key = PyXmlSec_NewKey1((PyTypeObject*)self)) == NULL) goto ON_FAIL; + + Py_BEGIN_ALLOW_THREADS; + #if XMLSEC_VERSION_HEX >= 0x10303 + // from version 1.3.3 (inclusive) + key->handle = xmlSecCryptoAppKeyLoadEx(engine_and_key_id, xmlSecKeyDataTypePrivate, xmlSecKeyDataFormatEngine, NULL, xmlSecCryptoAppGetDefaultPwdCallback(), (void*)engine_and_key_id); + #else + key->handle = xmlSecCryptoAppKeyLoad(engine_and_key_id, xmlSecKeyDataFormatEngine, NULL, xmlSecCryptoAppGetDefaultPwdCallback(), (void*)engine_and_key_id); + #endif + Py_END_ALLOW_THREADS; + + if (key->handle == NULL) { + PyXmlSec_SetLastError("cannot read key"); + goto ON_FAIL; + } + + key->is_own = 1; + + PYXMLSEC_DEBUG("load key from engine - ok"); + return (PyObject*)key; + +ON_FAIL: + PYXMLSEC_DEBUG("load key from engine - fail"); + Py_XDECREF(key); + return NULL; +} + +static const char PyXmlSec_KeyGenerate__doc__[] = \ + "generate(klass, size, type) -> xmlsec.Key\n" + "Generates key of kind ``klass`` with ``size`` and ``type``.\n\n" + ":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" + ":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[] = { "data", "size", "type", NULL}; + static char *kwlist[] = { "klass", "size", "type", NULL}; PyXmlSec_KeyData* keydata = NULL; short unsigned int keysize = 0; @@ -196,23 +279,32 @@ static PyObject* PyXmlSec_KeyGenerate(PyObject* self, PyObject* args, PyObject* return NULL; } -static const char PyXmlSec_KeyFromBinaryFile__doc__[] = "Loads (symmetric) key of kind *data* from *filename*.\n"; +static const char PyXmlSec_KeyFromBinaryFile__doc__[] = \ + "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" + ":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[] = { "data", "filename", NULL}; + static char *kwlist[] = { "klass", "filename", NULL}; PyXmlSec_KeyData* keydata = NULL; PyObject* filepath = NULL; PyXmlSec_Key* key = NULL; + const char* filename; 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; } - const char* filename = PyBytes_AsString(filepath); + filename = PyBytes_AsString(filepath); if (filename == NULL) goto ON_FAIL; if ((key = PyXmlSec_NewKey1((PyTypeObject*)self)) == NULL) goto ON_FAIL; @@ -234,27 +326,80 @@ static PyObject* PyXmlSec_KeyFromBinaryFile(PyObject* self, PyObject* args, PyOb ON_FAIL: PYXMLSEC_DEBUG("load symmetric key - fail"); Py_XDECREF(key); - Py_DECREF(filepath); + Py_XDECREF(filepath); return NULL; } -static const char PyXmlSec_KeyCertFromMemory__doc__[] = "Load certificate from memory.\n"; +static const char PyXmlSec_KeyFromBinaryData__doc__[] = \ + "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" + ":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}; + + PyXmlSec_KeyData* keydata = NULL; + const char* data = NULL; + Py_ssize_t data_size = 0; + + PyXmlSec_Key* key = NULL; + + PYXMLSEC_DEBUG("load symmetric key from memory - start"); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!s#:from_binary_data", kwlist, + PyXmlSec_KeyDataType, &keydata, &data, &data_size)) + { + goto ON_FAIL; + } + + if ((key = PyXmlSec_NewKey1((PyTypeObject*)self)) == NULL) goto ON_FAIL; + + Py_BEGIN_ALLOW_THREADS; + key->handle = xmlSecKeyReadMemory(keydata->id, (const xmlSecByte*)data, (xmlSecSize)data_size); + Py_END_ALLOW_THREADS; + + if (key->handle == NULL) { + PyXmlSec_SetLastError("cannot read key"); + goto ON_FAIL; + } + + key->is_own = 1; + + PYXMLSEC_DEBUG("load symmetric key from memory - ok"); + return (PyObject*)key; + +ON_FAIL: + PYXMLSEC_DEBUG("load symmetric key from memory - fail"); + Py_XDECREF(key); + return NULL; +} + +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" + ":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}; + PyXmlSec_Key* key = (PyXmlSec_Key*)self; const char* data = NULL; Py_ssize_t data_size = 0; unsigned int format = 0; PyObject* tmp = NULL; + int rv = 0; PYXMLSEC_DEBUGF("%p: load certificate from memory - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s#I:load_cert_from_memory", kwlist, &data, &data_size, &format)) { goto ON_FAIL; } - PyXmlSec_Key* key = (PyXmlSec_Key*)self; - int rv = 0; Py_BEGIN_ALLOW_THREADS; rv = xmlSecCryptoAppKeyCertLoadMemory(key->handle, (const xmlSecByte*)data, (xmlSecSize)data_size, format); Py_END_ALLOW_THREADS; @@ -271,9 +416,18 @@ static PyObject* PyXmlSec_KeyCertFromMemory(PyObject* self, PyObject* args, PyOb return NULL; } -static const char PyXmlSec_KeyCertFromFile__doc__[] = "Load certificate from file.\n"; +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" + ":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[] = { "data", "format", NULL}; + static char *kwlist[] = { "file", "format", NULL}; + + PyXmlSec_Key* key = (PyXmlSec_Key*)self; PyObject* file = NULL; unsigned int format = 0; @@ -282,6 +436,7 @@ static PyObject* PyXmlSec_KeyCertFromFile(PyObject* self, PyObject* args, PyObje int is_content = 0; const char* data = NULL; Py_ssize_t data_size = 0; + int rv = 0; PYXMLSEC_DEBUGF("%p: load certificate from memory - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OI:load_cert_from_file", kwlist, &file, &format)) { @@ -298,8 +453,6 @@ static PyObject* PyXmlSec_KeyCertFromFile(PyObject* self, PyObject* args, PyObje if (data == NULL) goto ON_FAIL; - PyXmlSec_Key* key = (PyXmlSec_Key*)self; - int rv = 0; Py_BEGIN_ALLOW_THREADS; if (is_content) { rv = xmlSecCryptoAppKeyCertLoadMemory(key->handle, (const xmlSecByte*)data, (xmlSecSize)data_size, format); @@ -321,30 +474,49 @@ 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; + PYXMLSEC_DEBUGF("%p: get name of key", self); - xmlSecKeyPtr handle = ((PyXmlSec_Key*)self)->handle; - if (handle == NULL) { + if (key->handle == NULL) { PyErr_SetString(PyExc_ValueError, "key is not ready"); return NULL; } - return PyString_FromString((const char*)xmlSecKeyGetName(handle)); + cname = (const char*)xmlSecKeyGetName(key->handle); + if (cname != NULL) { + return PyUnicode_FromString(cname); + } + Py_RETURN_NONE; } static int PyXmlSec_KeyNameSet(PyObject* self, PyObject* value, void* closure) { + PyXmlSec_Key* key = (PyXmlSec_Key*)self; + const char* name; + PYXMLSEC_DEBUGF("%p: set name of key %p", self, value); - xmlSecKeyPtr handle = ((PyXmlSec_Key*)self)->handle; - if (handle == NULL) { + if (key->handle == NULL) { PyErr_SetString(PyExc_ValueError, "key is not ready"); return -1; } - const char* 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(handle, XSTR(name)); + if (xmlSecKeySetName(key->handle, XSTR(name)) < 0) { + PyXmlSec_SetLastError("cannot set name"); + return -1; + } return 0; } @@ -372,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, @@ -384,6 +562,12 @@ static PyMethodDef PyXmlSec_KeyMethods[] = { METH_CLASS|METH_VARARGS|METH_KEYWORDS, PyXmlSec_KeyFromBinaryFile__doc__ }, + { + "from_binary_data", + (PyCFunction)PyXmlSec_KeyFromBinaryData, + METH_CLASS|METH_VARARGS|METH_KEYWORDS, + PyXmlSec_KeyFromBinaryData__doc__ + }, { "load_cert_from_memory", (PyCFunction)PyXmlSec_KeyCertFromMemory, @@ -448,9 +632,9 @@ static PyTypeObject _PyXmlSec_KeyType = { 0, /* tp_descr_set */ 0, /* tp_dictoffset */ 0, /* tp_init */ - PyType_GenericAlloc, /* tp_alloc */ + 0, /* tp_alloc */ PyXmlSec_Key__new__, /* tp_new */ - PyObject_Del /* tp_free */ + 0, /* tp_free */ }; PyTypeObject* PyXmlSec_KeyType = &_PyXmlSec_KeyType; @@ -472,15 +656,16 @@ static PyObject* PyXmlSec_KeysManager__new__(PyTypeObject *type, PyObject *args, } static int PyXmlSec_KeysManager__init__(PyObject* self, PyObject* args, PyObject* kwargs) { - PYXMLSEC_DEBUGF("%p: init key manager", self); xmlSecKeysMngrPtr handle = xmlSecKeysMngrCreate(); + + 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); @@ -489,19 +674,29 @@ static int PyXmlSec_KeysManager__init__(PyObject* self, PyObject* args, PyObject } static void PyXmlSec_KeysManager__del__(PyObject* self) { + PyXmlSec_KeysManager* mgr = (PyXmlSec_KeysManager*)self; + PYXMLSEC_DEBUGF("%p: delete KeysManager", self); - PyXmlSec_KeysManager* manager = (PyXmlSec_KeysManager*)self; - if (manager->handle != NULL) { - xmlSecKeysMngrDestroy(manager->handle); + + if (mgr->handle != NULL) { + PYXMLSEC_DEBUGF("%p: delete KeysManager handle - %p", self, mgr->handle); + xmlSecKeysMngrDestroy(mgr->handle); } Py_TYPE(self)->tp_free(self); } -static const char PyXmlSec_KeysManagerAddKey__doc__[] = "Adds a copy of *key*.\n"; +static const char PyXmlSec_KeysManagerAddKey__doc__[] = \ + "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}; - PyXmlSec_Key* key; + PyXmlSec_KeysManager* mgr = (PyXmlSec_KeysManager*)self; + PyXmlSec_Key* key = NULL; + xmlSecKeyPtr key2; + int rv; PYXMLSEC_DEBUGF("%p(%p): add key - start", self, ((PyXmlSec_KeysManager*)self)->handle); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!:add_key", kwlist, PyXmlSec_KeyType, &key)) { @@ -513,7 +708,6 @@ static PyObject* PyXmlSec_KeysManagerAddKey(PyObject* self, PyObject* args, PyOb goto ON_FAIL; } - xmlSecKeyPtr key2; Py_BEGIN_ALLOW_THREADS key2 = xmlSecKeyDuplicate(key->handle); Py_END_ALLOW_THREADS; @@ -523,9 +717,8 @@ static PyObject* PyXmlSec_KeysManagerAddKey(PyObject* self, PyObject* args, PyOb goto ON_FAIL; } - int rv; Py_BEGIN_ALLOW_THREADS; - rv = xmlSecCryptoAppDefaultKeysMngrAdoptKey(((PyXmlSec_KeysManager*)self)->handle, key2); + rv = xmlSecCryptoAppDefaultKeysMngrAdoptKey(mgr->handle, key2); Py_END_ALLOW_THREADS; if (rv < 0) { PyXmlSec_SetLastError("cannot add key"); @@ -539,52 +732,77 @@ static PyObject* PyXmlSec_KeysManagerAddKey(PyObject* self, PyObject* args, PyOb return NULL; } -static const char PyXmlSec_KeysManagerLoadCert__doc__[] = "load certificate from *filename*\n*format* - file format\n*type* - key type.\n"; +static const char PyXmlSec_KeysManagerLoadCert__doc__[] = \ + "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" + ":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}; - const char* filename = NULL; + PyXmlSec_KeysManager* mgr = (PyXmlSec_KeysManager*)self; + PyObject* filepath = NULL; unsigned int format = 0; unsigned int type = 0; + const char* filename; + int rv; + PYXMLSEC_DEBUGF("%p: load cert - start", self); - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sII:load_cert", kwlist, &filename, &format, &type)) { + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&II:load_cert", kwlist, + PyUnicode_FSConverter, &filepath, &format, &type)) { goto ON_FAIL; } - int rv; + filename = PyBytes_AsString(filepath); + Py_BEGIN_ALLOW_THREADS; - rv = xmlSecCryptoAppKeysMngrCertLoad(((PyXmlSec_KeysManager*)self)->handle, filename, format, type); + rv = xmlSecCryptoAppKeysMngrCertLoad(mgr->handle, filename, format, type); Py_END_ALLOW_THREADS; if (rv < 0) { PyXmlSec_SetLastError("cannot load cert"); goto ON_FAIL; } + Py_DECREF(filepath); PYXMLSEC_DEBUGF("%p: load cert - ok", self); Py_RETURN_NONE; ON_FAIL: PYXMLSEC_DEBUGF("%p: load cert - fail", self); + Py_XDECREF(filepath); return NULL; } -static const char PyXmlSec_KeysManagerLoadCertFromMemory__doc__[] = "load certificate from *data*\n*format* - file format\n*type* - key type.\n"; +static const char PyXmlSec_KeysManagerLoadCertFromMemory__doc__[] = \ + "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" + ":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}; + PyXmlSec_KeysManager* mgr = (PyXmlSec_KeysManager*)self; + const char* data = NULL; unsigned int type = 0; unsigned int format = 0; Py_ssize_t data_size = 0; + int rv; PYXMLSEC_DEBUGF("%p: load cert from memory - start", self); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s#II:load_cert", kwlist, &data, &data_size, &format, &type)) { goto ON_FAIL; } - xmlSecKeysMngrPtr handle = ((PyXmlSec_KeysManager*)self)->handle; - int rv; Py_BEGIN_ALLOW_THREADS; - rv = xmlSecCryptoAppKeysMngrCertLoadMemory(handle, (const xmlSecByte*)data, (xmlSecSize)data_size, format, type); + rv = xmlSecCryptoAppKeysMngrCertLoadMemory(mgr->handle, (const xmlSecByte*)data, (xmlSecSize)data_size, format, type); Py_END_ALLOW_THREADS; if (rv < 0) { PyXmlSec_SetLastError("cannot load cert from memory"); @@ -656,9 +874,9 @@ static PyTypeObject _PyXmlSec_KeysManagerType = { 0, /* tp_descr_set */ 0, /* tp_dictoffset */ PyXmlSec_KeysManager__init__, /* tp_init */ - PyType_GenericAlloc, /* tp_alloc */ + 0, /* tp_alloc */ PyXmlSec_KeysManager__new__, /* tp_new */ - PyObject_Del /* tp_free */ + 0, /* tp_free */ }; PyTypeObject* PyXmlSec_KeysManagerType = &_PyXmlSec_KeysManagerType; diff --git a/src/lxml.c b/src/lxml.c index 3f48135d..c98e933b 100644 --- a/src/lxml.c +++ b/src/lxml.c @@ -9,18 +9,111 @@ #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(); } +int PyXmlSec_IsElement(xmlNodePtr xnode) { + return _isElement(xnode); +} + PyXmlSec_LxmlElementPtr PyXmlSec_elementFactory(PyXmlSec_LxmlDocumentPtr doc, xmlNodePtr xnode) { return elementFactory(doc, xnode); } diff --git a/src/lxml.h b/src/lxml.h index f5838d67..72050efe 100644 --- a/src/lxml.h +++ b/src/lxml.h @@ -16,16 +16,26 @@ #include #include -#include -#include +#include typedef struct LxmlElement* PyXmlSec_LxmlElementPtr; typedef struct LxmlDocument* PyXmlSec_LxmlDocumentPtr; +// checks that xnode is Element +int PyXmlSec_IsElement(xmlNodePtr xnode); // creates a new element PyXmlSec_LxmlElementPtr PyXmlSec_elementFactory(PyXmlSec_LxmlDocumentPtr doc, xmlNodePtr node); // 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 26842c26..61eac139 100644 --- a/src/main.c +++ b/src/main.c @@ -10,52 +10,68 @@ #include "common.h" #include "platform.h" #include "exception.h" +#include "lxml.h" #include #include #include +#include +#include -#define _FREE_NONE 0 -#define _FREE_XMLSEC 1 -#define _FREE_ALL 2 +#define _PYXMLSEC_FREE_NONE 0 +#define _PYXMLSEC_FREE_XMLSEC 1 +#define _PYXMLSEC_FREE_CRYPTOLIB 2 +#define _PYXMLSEC_FREE_ALL 3 -static int free_mode = _FREE_NONE; +static int free_mode = _PYXMLSEC_FREE_NONE; + +#define MODULE_DOC "The tiny python wrapper around xmlsec1 (" XMLSEC_VERSION ") library" + +#ifndef XMLSEC_NO_CRYPTO_DYNAMIC_LOADING +static const xmlChar* PyXmlSec_GetCryptoLibName() { +#if XMLSEC_VERSION_HEX > 0x10214 + // xmlSecGetDefaultCrypto was introduced in version 1.2.21 + const xmlChar* cryptoLib = xmlSecGetDefaultCrypto(); +#else + const xmlChar* cryptoLib = (const xmlChar*) XMLSEC_CRYPTO; +#endif + PYXMLSEC_DEBUGF("dynamic crypto library: %s", cryptoLib); + return cryptoLib; +} +#endif // !XMLSEC_NO_CRYPTO_DYNAMIC_LOADING static void PyXmlSec_Free(int what) { PYXMLSEC_DEBUGF("free resources %d", what); switch (what) { - case _FREE_ALL: + case _PYXMLSEC_FREE_ALL: xmlSecCryptoAppShutdown(); - case _FREE_XMLSEC: + case _PYXMLSEC_FREE_CRYPTOLIB: +#ifndef XMLSEC_NO_CRYPTO_DYNAMIC_LOADING + xmlSecCryptoDLUnloadLibrary(PyXmlSec_GetCryptoLibName()); +#endif + case _PYXMLSEC_FREE_XMLSEC: xmlSecShutdown(); } - free_mode = _FREE_NONE; + free_mode = _PYXMLSEC_FREE_NONE; } static int PyXmlSec_Init(void) { if (xmlSecInit() < 0) { PyXmlSec_SetLastError("cannot initialize xmlsec library."); - PyXmlSec_Free(_FREE_NONE); + PyXmlSec_Free(_PYXMLSEC_FREE_NONE); return -1; } if (xmlSecCheckVersion() != 1) { PyXmlSec_SetLastError("xmlsec library version mismatch."); - PyXmlSec_Free(_FREE_XMLSEC); + PyXmlSec_Free(_PYXMLSEC_FREE_XMLSEC); return -1; } #ifndef XMLSEC_NO_CRYPTO_DYNAMIC_LOADING -#if XMLSEC_VERSION_HEX > 308 - // xmlSecGetDefaultCrypto was introduced in version 1.2.21 - const xmlChar* cryptoLib = xmlSecGetDefaultCrypto(); -#else - const xmlChar* cryptoLib = (const xmlChar*) XMLSEC_CRYPTO; -#endif - PYXMLSEC_DEBUGF("dynamic crypto library: %s", cryptoLib); - if (xmlSecCryptoDLLoadLibrary(cryptoLib) < 0) { + if (xmlSecCryptoDLLoadLibrary(PyXmlSec_GetCryptoLibName()) < 0) { PyXmlSec_SetLastError("cannot load crypto library for xmlsec."); - PyXmlSec_Free(_FREE_XMLSEC); + PyXmlSec_Free(_PYXMLSEC_FREE_XMLSEC); return -1; } #endif /* XMLSEC_CRYPTO_DYNAMIC_LOADING */ @@ -63,24 +79,30 @@ static int PyXmlSec_Init(void) { /* Init crypto library */ if (xmlSecCryptoAppInit(NULL) < 0) { PyXmlSec_SetLastError("cannot initialize crypto library application."); - PyXmlSec_Free(_FREE_XMLSEC); + PyXmlSec_Free(_PYXMLSEC_FREE_CRYPTOLIB); return -1; } /* Init xmlsec-crypto library */ if (xmlSecCryptoInit() < 0) { PyXmlSec_SetLastError("cannot initialize crypto library."); - PyXmlSec_Free(_FREE_ALL); + PyXmlSec_Free(_PYXMLSEC_FREE_ALL); return -1; } - free_mode = _FREE_ALL; + // 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__[] = -"Initialize the library for general operation.\n" \ -"This is called upon library import and does not need to be called\n" \ -"again (unless @ref _shutdown is called explicitly).\n"; +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 :func:`~.shutdown` is called explicitly).\n"; static PyObject* PyXmlSec_PyInit(PyObject *self) { if (PyXmlSec_Init() < 0) { return NULL; @@ -88,24 +110,298 @@ static PyObject* PyXmlSec_PyInit(PyObject *self) { Py_RETURN_NONE; } -static char PyXmlSec_PyShutdown__doc__[] = -"Shutdown the library and cleanup any leftover resources.\n" \ -"This is called automatically upon interpreter termination and\n" \ -"should not need to be called explicitly."; +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."; static PyObject* PyXmlSec_PyShutdown(PyObject* self) { - PyXmlSec_Free(_FREE_ALL); + PyXmlSec_Free(free_mode); Py_RETURN_NONE; } -static char PyXmlSec_PyEnableDebugOutput__doc__[] = -"Enables or disables calling LibXML2 callback from the default errors callback.\n"; +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 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; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O:enable_debug_trace", kwlist, &enabled)) { return NULL; } - xmlSecErrorsDefaultCallbackEnableOutput(PyObject_IsTrue(enabled)); + PyXmlSecEnableDebugTrace(PyObject_IsTrue(enabled)); + 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; } @@ -122,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 */ }; @@ -149,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; @@ -159,7 +495,7 @@ static int PyXmlSec_PyClear(PyObject *self) { static PyModuleDef PyXmlSecModule = { PyModuleDef_HEAD_INIT, STRINGIFY(MODULE_NAME), /* name of module */ - STRINGIFY(MODULE_DOC), /* module documentation, may be NULL */ + MODULE_DOC, /* module documentation, may be NULL */ -1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */ PyXmlSec_MainMethods, /* m_methods */ @@ -171,59 +507,20 @@ 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, STRINGIFY(MODULE_DOC)); -#endif if (!module) { PY_MOD_RETURN(NULL); /* this really should never happen */ } PYXMLSEC_DEBUGF("%p", module); + // init first, since PyXmlSec_Init may raise XmlSecError + if (PyXmlSec_ExceptionsModule_Init(module) < 0) goto ON_FAIL; + if (PyXmlSec_Init() < 0) goto ON_FAIL; if (PyModule_AddStringConstant(module, "__version__", STRINGIFY(MODULE_VERSION)) < 0) goto ON_FAIL; @@ -231,20 +528,12 @@ PYENTRY_FUNC_NAME(void) if (PyXmlSec_InitLxmlModule() < 0) goto ON_FAIL; /* Populate final object settings */ if (PyXmlSec_ConstantsModule_Init(module) < 0) goto ON_FAIL; - if (PyXmlSec_ExceptionsModule_Init(module) < 0) goto ON_FAIL; if (PyXmlSec_KeyModule_Init(module) < 0) goto ON_FAIL; if (PyXmlSec_TreeModule_Init(module) < 0) goto ON_FAIL; if (PyXmlSec_DSModule_Init(module) < 0) goto ON_FAIL; 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 eae902b9..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,49 +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_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 cc67b244..ae0eca34 100644 --- a/src/template.c +++ b/src/template.c @@ -18,27 +18,40 @@ #define PYXMLSEC_TEMPLATES_DOC "Xml Templates processing" static char PyXmlSec_TemplateCreate__doc__[] = \ - "Creates new node with the mandatory , ,\n" - " and children and sub-children.\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" + ":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", "name", "ns", NULL}; + static char *kwlist[] = { "node", "c14n_method", "sign_method", "id", "ns", "name", NULL}; PyXmlSec_LxmlElementPtr node = NULL; PyXmlSec_Transform* c14n = NULL; PyXmlSec_Transform* sign = NULL; - const char* name = NULL; + const char* id = NULL; const char* ns = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template create - start"); - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O!O!|zz:create", kwlist, - PyXmlSec_LxmlElementConverter, &node, PyXmlSec_TransformType, &c14n, PyXmlSec_TransformType, &sign, &name, &ns)) + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O!O!|zzz:create", kwlist, + PyXmlSec_LxmlElementConverter, &node, PyXmlSec_TransformType, &c14n, PyXmlSec_TransformType, &sign, &id, &ns, &id)) { goto ON_FAIL; } - xmlNodePtr res; Py_BEGIN_ALLOW_THREADS; - res = xmlSecTmplSignatureCreateNsPref(node->_doc->_c_doc, c14n->id, sign->id, XSTR(name), XSTR(ns)); + res = xmlSecTmplSignatureCreateNsPref(node->_doc->_c_doc, c14n->id, sign->id, XSTR(id), XSTR(ns)); Py_END_ALLOW_THREADS; if (res == NULL) { PyXmlSec_SetLastError("cannot create template."); @@ -54,8 +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"; + "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" + ":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" + ":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}; @@ -64,6 +90,7 @@ static PyObject* PyXmlSec_TemplateAddReference(PyObject* self, PyObject *args, P const char* id = NULL; const char* uri = NULL; const char* type = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template add_reference - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O!|zzz:add_reference", kwlist, @@ -71,7 +98,6 @@ static PyObject* PyXmlSec_TemplateAddReference(PyObject* self, PyObject *args, P { goto ON_FAIL; } - xmlNodePtr res; Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplSignatureAddReference(node->_c_node, digest->id, XSTR(id), XSTR(uri), XSTR(type)); Py_END_ALLOW_THREADS; @@ -89,12 +115,20 @@ static PyObject* PyXmlSec_TemplateAddReference(PyObject* self, PyObject *args, P } static char PyXmlSec_TemplateAddTransform__doc__[] = \ - "Adds node to the node of *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" + ":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}; PyXmlSec_LxmlElementPtr node = NULL; PyXmlSec_Transform* transform = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template add_transform - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O!:add_transform", kwlist, @@ -102,7 +136,6 @@ static PyObject* PyXmlSec_TemplateAddTransform(PyObject* self, PyObject *args, P { goto ON_FAIL; } - xmlNodePtr res; Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplReferenceAddTransform(node->_c_node, transform->id); Py_END_ALLOW_THREADS; @@ -120,19 +153,27 @@ static PyObject* PyXmlSec_TemplateAddTransform(PyObject* self, PyObject *args, P } static char PyXmlSec_TemplateEnsureKeyInfo__doc__[] = \ - "Adds (if necessary) node to the node of *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" + ":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}; PyXmlSec_LxmlElementPtr node = NULL; const char* id = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template ensure_key_info - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&|z:ensure_key_info", kwlist, PyXmlSec_LxmlElementConverter, &node, &id)) { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplSignatureEnsureKeyInfo(node->_c_node, XSTR(id)); Py_END_ALLOW_THREADS; @@ -150,12 +191,20 @@ static PyObject* PyXmlSec_TemplateEnsureKeyInfo(PyObject* self, PyObject *args, } static char PyXmlSec_TemplateAddKeyName__doc__[] = \ - "Adds node to the node of *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" + ":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}; PyXmlSec_LxmlElementPtr node = NULL; const char* name = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template add_key_name - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&|z:add_key_name", kwlist, PyXmlSec_LxmlElementConverter, &node, &name)) @@ -163,7 +212,6 @@ static PyObject* PyXmlSec_TemplateAddKeyName(PyObject* self, PyObject *args, PyO goto ON_FAIL; } - xmlNodePtr res; Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplKeyInfoAddKeyName(node->_c_node, XSTR(name)); Py_END_ALLOW_THREADS; @@ -181,11 +229,17 @@ static PyObject* PyXmlSec_TemplateAddKeyName(PyObject* self, PyObject *args, PyO } static char PyXmlSec_TemplateAddKeyValue__doc__[] = \ - "Adds node to the node of *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}; PyXmlSec_LxmlElementPtr node = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template add_key_value - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:add_key_value", kwlist, PyXmlSec_LxmlElementConverter, &node)) @@ -193,7 +247,6 @@ static PyObject* PyXmlSec_TemplateAddKeyValue(PyObject* self, PyObject *args, Py goto ON_FAIL; } - xmlNodePtr res; Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplKeyInfoAddKeyValue(node->_c_node); Py_END_ALLOW_THREADS; @@ -211,11 +264,17 @@ static PyObject* PyXmlSec_TemplateAddKeyValue(PyObject* self, PyObject *args, Py } static char PyXmlSec_TemplateAddX509Data__doc__[] = \ - "Adds node to the node of *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}; PyXmlSec_LxmlElementPtr node = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template add_x509_data - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:add_x509_data", kwlist, PyXmlSec_LxmlElementConverter, &node)) @@ -223,7 +282,6 @@ static PyObject* PyXmlSec_TemplateAddX509Data(PyObject* self, PyObject *args, Py goto ON_FAIL; } - xmlNodePtr res; Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplKeyInfoAddX509Data(node->_c_node); Py_END_ALLOW_THREADS; @@ -241,11 +299,17 @@ static PyObject* PyXmlSec_TemplateAddX509Data(PyObject* self, PyObject *args, Py } static char PyXmlSec_TemplateAddX509DataAddIssuerSerial__doc__[] = \ - "Adds node to the given node of *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}; PyXmlSec_LxmlElementPtr node = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template x509_data_add_issuer_serial - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:x509_data_add_issuer_serial", kwlist, @@ -253,7 +317,6 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddIssuerSerial(PyObject* self, PyO { goto ON_FAIL; } - xmlNodePtr res; Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplX509DataAddIssuerSerial(node->_c_node); Py_END_ALLOW_THREADS; @@ -271,12 +334,20 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddIssuerSerial(PyObject* self, PyO } static char PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerName__doc__[] = \ - "Adds node to the node of *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" + ":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}; PyXmlSec_LxmlElementPtr node = NULL; const char* name = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template x509_issuer_serial_add_issuer_name - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&|z:x509_issuer_serial_add_issuer_name", kwlist, @@ -284,7 +355,7 @@ static PyObject* PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerName(PyObject* { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplX509IssuerSerialAddIssuerName(node->_c_node, XSTR(name)); Py_END_ALLOW_THREADS; @@ -302,12 +373,20 @@ static PyObject* PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerName(PyObject* } static char PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerSerialNumber__doc__[] = \ - "Adds node to the node of *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" + ":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}; PyXmlSec_LxmlElementPtr node = NULL; const char* serial = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template x509_issuer_serial_add_serial_number - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&|z:x509_issuer_serial_add_serial_number", kwlist, @@ -315,7 +394,7 @@ static PyObject* PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerSerialNumber(P { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplX509IssuerSerialAddSerialNumber(node->_c_node, XSTR(serial)); Py_END_ALLOW_THREADS; @@ -333,11 +412,17 @@ static PyObject* PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerSerialNumber(P } static char PyXmlSec_TemplateAddX509DataAddSubjectName__doc__[] = \ - "Adds node to the given node of *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}; PyXmlSec_LxmlElementPtr node = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template x509_data_add_subject_name - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:x509_data_add_subject_name", kwlist, @@ -345,7 +430,7 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddSubjectName(PyObject* self, PyOb { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplX509DataAddSubjectName(node->_c_node); Py_END_ALLOW_THREADS; @@ -363,11 +448,17 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddSubjectName(PyObject* self, PyOb } static char PyXmlSec_TemplateAddX509DataAddSKI__doc__[] = \ - "Adds node to the given node of *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}; PyXmlSec_LxmlElementPtr node = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template x509_data_add_ski - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:x509_data_add_ski", kwlist, @@ -375,7 +466,7 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddSKI(PyObject* self, PyObject *ar { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplX509DataAddSKI(node->_c_node); Py_END_ALLOW_THREADS; @@ -393,11 +484,17 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddSKI(PyObject* self, PyObject *ar } static char PyXmlSec_TemplateAddX509DataAddCertificate__doc__[] = \ - "Adds node to the given node of *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}; PyXmlSec_LxmlElementPtr node = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template x509_data_add_certificate - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:x509_data_add_certificate", kwlist, @@ -405,7 +502,7 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddCertificate(PyObject* self, PyOb { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplX509DataAddCertificate(node->_c_node); Py_END_ALLOW_THREADS; @@ -423,11 +520,17 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddCertificate(PyObject* self, PyOb } static char PyXmlSec_TemplateAddX509DataAddCRL__doc__[] = \ - "Adds node to the given node of *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}; PyXmlSec_LxmlElementPtr node = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template x509_data_add_crl - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:x509_data_add_crl", kwlist, @@ -435,7 +538,7 @@ static PyObject* PyXmlSec_TemplateAddX509DataAddCRL(PyObject* self, PyObject *ar { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplX509DataAddCRL(node->_c_node); Py_END_ALLOW_THREADS; @@ -453,7 +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"; + "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" + ":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}; @@ -462,6 +578,7 @@ static PyObject* PyXmlSec_TemplateAddEncryptedKey(PyObject* self, PyObject *args const char* id = NULL; const char* type = NULL; const char* recipient = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template add_encrypted_key - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O!|zzz:add_encrypted_key", kwlist, @@ -469,7 +586,7 @@ static PyObject* PyXmlSec_TemplateAddEncryptedKey(PyObject* self, PyObject *args { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplKeyInfoAddEncryptedKey(node->_c_node, method->id, XSTR(id), XSTR(type), XSTR(recipient)); Py_END_ALLOW_THREADS; @@ -487,7 +604,24 @@ static PyObject* PyXmlSec_TemplateAddEncryptedKey(PyObject* self, PyObject *args } static char PyXmlSec_TemplateCreateEncryptedData__doc__[] = \ - "Creates new <{ns}:EncryptedData /> node for encryption template.\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" + ":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" + ":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}; @@ -498,6 +632,7 @@ static PyObject* PyXmlSec_TemplateCreateEncryptedData(PyObject* self, PyObject * const char* mime_type = NULL; const char* encoding = NULL; const char* ns = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template encrypted_data_create - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O!|zzzzz:encrypted_data_create", kwlist, @@ -505,7 +640,7 @@ static PyObject* PyXmlSec_TemplateCreateEncryptedData(PyObject* self, PyObject * { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplEncDataCreate(node->_doc->_c_doc, method->id, XSTR(id), XSTR(type), XSTR(mime_type), XSTR(encoding)); Py_END_ALLOW_THREADS; @@ -526,13 +661,23 @@ static PyObject* PyXmlSec_TemplateCreateEncryptedData(PyObject* self, PyObject * } static char PyXmlSec_TemplateEncryptedDataEnsureKeyInfo__doc__[] = \ - "Adds <{ns}:KeyInfo/> to the node of *node*.\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" + ":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}; PyXmlSec_LxmlElementPtr node = NULL; const char* id = NULL; const char* ns = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template encrypted_data_ensure_key_info - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&|zz:encrypted_data_ensure_key_info", kwlist, @@ -540,7 +685,7 @@ static PyObject* PyXmlSec_TemplateEncryptedDataEnsureKeyInfo(PyObject* self, PyO { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplEncDataEnsureKeyInfo(node->_c_node, XSTR(id)); Py_END_ALLOW_THREADS; @@ -561,11 +706,17 @@ static PyObject* PyXmlSec_TemplateEncryptedDataEnsureKeyInfo(PyObject* self, PyO } static char PyXmlSec_TemplateEncryptedDataEnsureCipherValue__doc__[] = \ - "Adds to the node of *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}; PyXmlSec_LxmlElementPtr node = NULL; + xmlNodePtr res; PYXMLSEC_DEBUG("template encrypted_data_ensure_cipher_value - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:encrypted_data_ensure_cipher_value", kwlist, @@ -573,7 +724,7 @@ static PyObject* PyXmlSec_TemplateEncryptedDataEnsureCipherValue(PyObject* self, { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecTmplEncDataEnsureCipherValue(node->_c_node); Py_END_ALLOW_THREADS; @@ -590,6 +741,64 @@ static PyObject* PyXmlSec_TemplateEncryptedDataEnsureCipherValue(PyObject* self, return NULL; } +static char PyXmlSec_TemplateTransformAddC14NInclNamespaces__doc__[] = \ + "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}; + + PyXmlSec_LxmlElementPtr node = NULL; + PyObject* prefixes = NULL; + PyObject* sep; + int res; + const char* c_prefixes; + + // transform_add_c14n_inclusive_namespaces + PYXMLSEC_DEBUG("template encrypted_data_ensure_cipher_value - start"); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O:transform_add_c14n_inclusive_namespaces", kwlist, + PyXmlSec_LxmlElementConverter, &node, &prefixes)) + { + prefixes = NULL; + goto ON_FAIL; + } + if (PyList_Check(prefixes) || PyTuple_Check(prefixes)) { + sep = PyUnicode_FromString(" "); + prefixes = PyObject_CallMethod(sep, "join", "O", prefixes); + Py_DECREF(sep); + } else if (PyUnicode_Check(prefixes)) { + Py_INCREF(prefixes); + } else { + PyErr_SetString(PyExc_TypeError, "expected instance of str or list of str"); + prefixes = NULL; + } + + if (prefixes == NULL) { + goto ON_FAIL; + } + + + c_prefixes = PyUnicode_AsUTF8(prefixes); + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplTransformAddC14NInclNamespaces(node->_c_node, XSTR(c_prefixes)); + Py_END_ALLOW_THREADS; + if (res != 0) { + PyXmlSec_SetLastError("cannot add 'inclusive' namespaces to the ExcC14N transform node"); + goto ON_FAIL; + } + + Py_DECREF(prefixes); + PYXMLSEC_DEBUG("transform_add_c14n_inclusive_namespaces - ok"); + Py_RETURN_NONE; + +ON_FAIL: + PYXMLSEC_DEBUG("transform_add_c14n_inclusive_namespaces - fail"); + Py_XDECREF(prefixes); + return NULL; +} static PyMethodDef PyXmlSec_TemplateMethods[] = { { @@ -700,10 +909,15 @@ static PyMethodDef PyXmlSec_TemplateMethods[] = { METH_VARARGS|METH_KEYWORDS, PyXmlSec_TemplateEncryptedDataEnsureCipherValue__doc__ }, + { + "transform_add_c14n_inclusive_namespaces", + (PyCFunction)PyXmlSec_TemplateTransformAddC14NInclNamespaces, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateTransformAddC14NInclNamespaces__doc__, + }, {NULL, NULL} /* sentinel */ }; -#ifdef PY3K static PyModuleDef PyXmlSec_TemplateModule = { PyModuleDef_HEAD_INIT, @@ -716,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 b2fc4ddc..37cae785 100644 --- a/src/tree.c +++ b/src/tree.c @@ -16,13 +16,23 @@ #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"; + "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" + ":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}; PyXmlSec_LxmlElementPtr node = NULL; const char* name = NULL; const char* ns = (const char*)xmlSecDSigNs; + xmlNodePtr res; PYXMLSEC_DEBUG("tree find_child - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&s|s:find_child", kwlist, @@ -30,7 +40,7 @@ static PyObject* PyXmlSec_TreeFindChild(PyObject* self, PyObject *args, PyObject { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecFindChild(node->_c_node, XSTR(name), XSTR(ns)); Py_END_ALLOW_THREADS; @@ -47,13 +57,23 @@ 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"; + "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" + ":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}; PyXmlSec_LxmlElementPtr node = NULL; const char* name = NULL; const char* ns = (const char*)xmlSecDSigNs; + xmlNodePtr res; PYXMLSEC_DEBUG("tree find_parent - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&s|s:find_parent", kwlist, @@ -61,7 +81,7 @@ static PyObject* PyXmlSec_TreeFindParent(PyObject* self, PyObject *args, PyObjec { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecFindParent(node->_c_node, XSTR(name), XSTR(ns)); Py_END_ALLOW_THREADS; @@ -78,13 +98,23 @@ 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"; + "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" + ":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}; PyXmlSec_LxmlElementPtr node = NULL; const char* name = NULL; const char* ns = (const char*)xmlSecDSigNs; + xmlNodePtr res; PYXMLSEC_DEBUG("tree find_node - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&s|s:find_node", kwlist, @@ -92,7 +122,7 @@ static PyObject* PyXmlSec_TreeFindNode(PyObject* self, PyObject *args, PyObject { goto ON_FAIL; } - xmlNodePtr res; + Py_BEGIN_ALLOW_THREADS; res = xmlSecFindNode(node->_c_node, XSTR(name), XSTR(ns)); Py_END_ALLOW_THREADS; @@ -109,11 +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" - "the id inkey_data_formation is not available by other means (e.g. an associated DTD or XML schema).\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" + ":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}; @@ -122,12 +157,17 @@ static PyObject* PyXmlSec_TreeAddIds(PyObject* self, PyObject *args, PyObject *k const xmlChar** list = NULL; + Py_ssize_t n; + PyObject* tmp; + PyObject* key; + Py_ssize_t i; + PYXMLSEC_DEBUG("tree add_ids - start"); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O:add_ids", kwlist, PyXmlSec_LxmlElementConverter, &node, &ids)) { goto ON_FAIL; } - Py_ssize_t n = PyObject_Length(ids); + n = PyObject_Length(ids); if (n < 0) goto ON_FAIL; list = (const xmlChar**)xmlMalloc(sizeof(xmlChar*) * (n + 1)); @@ -136,15 +176,13 @@ static PyObject* PyXmlSec_TreeAddIds(PyObject* self, PyObject *args, PyObject *k goto ON_FAIL; } - PyObject* tmp; - PyObject* key; - for (Py_ssize_t i = 0; i < n; ++i) { + for (i = 0; i < n; ++i) { key = PyLong_FromSsize_t(i); if (key == NULL) goto ON_FAIL; 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; } @@ -192,7 +230,6 @@ static PyMethodDef PyXmlSec_TreeMethods[] = { {NULL, NULL} /* sentinel */ }; -#ifdef PY3K static PyModuleDef PyXmlSec_TreeModule = { PyModuleDef_HEAD_INIT, @@ -205,20 +242,13 @@ 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) ".template", PyXmlSec_TreeMethods, PYXMLSEC_TREE_DOC); - Py_XINCREF(tree); -#endif if (!tree) goto ON_FAIL; - PYXMLSEC_DEBUGF("%", tree); if (PyModule_AddObject(package, "tree", tree) < 0) goto ON_FAIL; return 0; diff --git a/src/utils.c b/src/utils.c index 679a1b2d..cdcb182b 100644 --- a/src/utils.c +++ b/src/utils.c @@ -10,10 +10,14 @@ #include "utils.h" PyObject* PyXmlSec_GetFilePathOrContent(PyObject* file, int* is_content) { + PyObject* data; + PyObject* utf8; + PyObject* tmp = NULL; + if (PyObject_HasAttrString(file, "read")) { - PyObject* data = PyObject_CallMethod(file, "read", NULL); + data = PyObject_CallMethod(file, "read", NULL); if (data != NULL && PyUnicode_Check(data)) { - PyObject* utf8 = PyUnicode_AsUTF8String(data); + utf8 = PyUnicode_AsUTF8String(data); Py_DECREF(data); data = utf8; } @@ -21,29 +25,32 @@ PyObject* PyXmlSec_GetFilePathOrContent(PyObject* file, int* is_content) { return data; } *is_content = 0; - PyObject* tmp = NULL; - 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) { return -1; } - int r = PyObject_SetAttrString(obj, name, tmp); + r = PyObject_SetAttrString(obj, name, tmp); Py_DECREF(tmp); return r; } int PyXmlSec_SetLongAttr(PyObject* obj, const char* name, long value) { PyObject* tmp = PyLong_FromLong(value); + int r; + if (tmp == NULL) { return -1; } - int r = PyObject_SetAttrString(obj, name, tmp); + r = PyObject_SetAttrString(obj, name, tmp); Py_DECREF(tmp); return r; } 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 new file mode 100644 index 00000000..1d21c89b --- /dev/null +++ b/tests/base.py @@ -0,0 +1,119 @@ +import gc +import os +import sys +import unittest + +from lxml import etree + +import xmlsec + +etype = type(etree.Element('test')) + +ns = {'dsig': xmlsec.constants.DSigNs, 'enc': xmlsec.constants.EncNs} + + +try: + import resource + + test_iterations = int(os.environ.get('PYXMLSEC_TEST_ITERATIONS', '10')) +except (ImportError, ValueError): + test_iterations = 0 + + +class TestMemoryLeaks(unittest.TestCase): + maxDiff = None + + iterations = test_iterations + + data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def setUp(self): + gc.disable() + self.addTypeEqualityFunc(etype, 'assertXmlEqual') + xmlsec.enable_debug_trace(1) + + def run(self, result=None): + # run first time + super().run(result=result) + if self.iterations == 0: + return + + m_usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + o_count = gc.get_count()[0] + m_hits = 0 + o_hits = 0 + 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 + m_hits += 1 + o_count_n = gc.get_count()[0] + if o_count_n > o_count: + o_count = o_count_n + o_hits += 1 + del m_usage_n + del o_count_n + + if m_hits > int(self.iterations * 0.8): + result.buffer = False + try: + 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') + except AssertionError: + result.addError(self, sys.exc_info()) + + def path(self, name): + """Return full path for resource.""" + return os.path.join(self.data_dir, name) + + def load(self, name): + """Load resource by name.""" + with open(self.path(name), 'rb') as stream: + return stream.read() + + def load_xml(self, name, xpath=None): + """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): + """Check equality of etree.roots.""" + msg = msg or '' + if first.tag != second.tag: + 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(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(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(f'children length differs, {len(cl1)} != {len(cl2)}. {msg}') + for c1, c2 in zip(cl1, cl2): + self.assertXmlEqual(c1, c2) + + +def _xml_text_compare(t1, t2): + if not t1 and not t2: + return True + if t1 == '*' or t2 == '*': + return True + return (t1 or '').strip() == (t2 or '').strip() diff --git a/tests/conftest.py b/tests/conftest.py index 60e9c64d..a65235d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ -# -*- coding: utf-8 -*- -import sys -from os import path +def pytest_collection_modifyitems(items): + """Put the module init test first. -# Get the base path. -base = path.join(path.dirname(__file__), '..') + This way, we implicitly check whether any subsequent test fails because of module reinitialization. + """ -# Append the source and test packages directories to PATH. -sys.path.append(path.join(base, 'src')) + 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 new file mode 100644 index 00000000..73245c3c --- /dev/null +++ b/tests/data/deskey.bin @@ -0,0 +1 @@ +012345670123456701234567 diff --git a/tests/data/doc.xml b/tests/data/doc.xml new file mode 100644 index 00000000..39f8d761 --- /dev/null +++ b/tests/data/doc.xml @@ -0,0 +1,7 @@ + + + +Hello, World! + diff --git a/tests/data/dsacert.der b/tests/data/dsacert.der new file mode 100644 index 00000000..0a5008b4 Binary files /dev/null and b/tests/data/dsacert.der differ diff --git a/tests/data/dsakey.der b/tests/data/dsakey.der new file mode 100644 index 00000000..a056304c Binary files /dev/null and b/tests/data/dsakey.der differ diff --git a/tests/data/enc1-in.xml b/tests/data/enc1-in.xml new file mode 100644 index 00000000..39f8d761 --- /dev/null +++ b/tests/data/enc1-in.xml @@ -0,0 +1,7 @@ + + + +Hello, World! + diff --git a/tests/data/enc1-out.xml b/tests/data/enc1-out.xml new file mode 100644 index 00000000..ab0b1a6c --- /dev/null +++ b/tests/data/enc1-out.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/tests/data/enc2-in.xml b/tests/data/enc2-in.xml new file mode 100644 index 00000000..e108afc7 --- /dev/null +++ b/tests/data/enc2-in.xml @@ -0,0 +1,2 @@ + +test diff --git a/tests/examples/enc2-res.xml b/tests/data/enc2-out.xml similarity index 98% rename from tests/examples/enc2-res.xml rename to tests/data/enc2-out.xml index 6556248e..4b3b5c34 100644 --- a/tests/examples/enc2-res.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/enc3-in.xml b/tests/data/enc3-in.xml new file mode 100644 index 00000000..a2695635 --- /dev/null +++ b/tests/data/enc3-in.xml @@ -0,0 +1,4 @@ + + +test + diff --git a/tests/data/enc3-out.xml b/tests/data/enc3-out.xml new file mode 100644 index 00000000..5289d7e9 --- /dev/null +++ b/tests/data/enc3-out.xml @@ -0,0 +1,20 @@ + + + + + +HJwrfL7kOIB0QaldMJdza1HitpLCjw+eoult1C6yExDXJ09zKaSQER+pUL9Vt5fm +d4Oitsf0CUNkjG1xWJdFsftqUIuvYGnkUNhT0vtqoYbdhJkCcB9cCwvTrww2+VTF +NIasTdechlSD1qQOR8uf6+S94Ae4PVSfWU+5YLTJFpMjR+OT7f6BSbYNv1By6Cko +G39WTSKTRcVDzcMxRepAGb59r508yKIJhwabCf3Opu+Ams7ia7BH4oa4ro9YSWwm +hAJ0CN4a6b5odcRbNvuHcwWSxpoysWKbOROQ0H4xC4nGZeL/AXlpSc8eNuNG+g6D +CTBwsOXCAEJYXPkTrnB3qQ== + + + + + +4m5BRKEswOe8JISY7NrPGLBYv7Ay5pBV+nG6it51gz0= + + + diff --git a/tests/data/enc_template.xml b/tests/data/enc_template.xml new file mode 100644 index 00000000..8db1ee95 --- /dev/null +++ b/tests/data/enc_template.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/data/rsacert.pem b/tests/data/rsacert.pem new file mode 100644 index 00000000..e8a68228 --- /dev/null +++ b/tests/data/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/tests/data/rsakey.pem b/tests/data/rsakey.pem new file mode 100644 index 00000000..55d2fd9b --- /dev/null +++ b/tests/data/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/tests/data/rsapub.pem b/tests/data/rsapub.pem new file mode 100644 index 00000000..838a346d --- /dev/null +++ b/tests/data/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/tests/data/sign1-in.xml b/tests/data/sign1-in.xml new file mode 100644 index 00000000..0a0cd442 --- /dev/null +++ b/tests/data/sign1-in.xml @@ -0,0 +1,26 @@ + + + + + Hello, World! + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/sign1-out.xml b/tests/data/sign1-out.xml new file mode 100644 index 00000000..f46ac1f4 --- /dev/null +++ b/tests/data/sign1-out.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/tests/examples/sign2-doc.xml b/tests/data/sign2-in.xml similarity index 92% rename from tests/examples/sign2-doc.xml rename to tests/data/sign2-in.xml index 5d9fb352..2f2592f2 100644 --- a/tests/examples/sign2-doc.xml +++ b/tests/data/sign2-in.xml @@ -1,6 +1,6 @@ - diff --git a/tests/examples/sign2-res.xml b/tests/data/sign2-out.xml similarity index 99% rename from tests/examples/sign2-res.xml rename to tests/data/sign2-out.xml index b37cad94..b5782d6c 100644 --- a/tests/examples/sign2-res.xml +++ b/tests/data/sign2-out.xml @@ -1,6 +1,6 @@ - diff --git a/tests/examples/sign3-doc.xml b/tests/data/sign3-in.xml similarity index 92% rename from tests/examples/sign3-doc.xml rename to tests/data/sign3-in.xml index f75da16a..96260b8f 100644 --- a/tests/examples/sign3-doc.xml +++ b/tests/data/sign3-in.xml @@ -1,6 +1,6 @@ - diff --git a/tests/examples/sign3-res.xml b/tests/data/sign3-out.xml similarity index 99% rename from tests/examples/sign3-res.xml rename to tests/data/sign3-out.xml index 847e1af2..b7bf15c3 100644 --- a/tests/examples/sign3-res.xml +++ b/tests/data/sign3-out.xml @@ -1,6 +1,6 @@ - diff --git a/tests/examples/sign4-doc.xml b/tests/data/sign4-in.xml similarity index 94% rename from tests/examples/sign4-doc.xml rename to tests/data/sign4-in.xml index cc00479b..d49fc3ae 100644 --- a/tests/examples/sign4-doc.xml +++ b/tests/data/sign4-in.xml @@ -1,6 +1,6 @@ - diff --git a/tests/examples/sign4-res.xml b/tests/data/sign4-out.xml similarity index 99% rename from tests/examples/sign4-res.xml rename to tests/data/sign4-out.xml index a6fecb44..bdd1014e 100644 --- a/tests/examples/sign4-res.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/examples/sign5-doc.xml b/tests/data/sign5-in.xml similarity index 100% rename from tests/examples/sign5-doc.xml rename to tests/data/sign5-in.xml 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/examples/sign5-res.xml b/tests/data/sign5-out.xml similarity index 100% rename from tests/examples/sign5-res.xml rename to tests/data/sign5-out.xml diff --git a/tests/data/sign6-in.bin b/tests/data/sign6-in.bin new file mode 100644 index 00000000..b6364f29 --- /dev/null +++ b/tests/data/sign6-in.bin @@ -0,0 +1 @@ +f4dP.ĆC>Źqc-B> \ No newline at end of file diff --git a/tests/data/sign6-out.bin b/tests/data/sign6-out.bin new file mode 100644 index 00000000..b6240dd0 --- /dev/null +++ b/tests/data/sign6-out.bin @@ -0,0 +1,3 @@ +h˱`exirQ U_G'c>27^`pˁұ ' +<@=}O8}fy*o{yo^h|<_7u_t/sTb ӓ'zLDㄈ6J[{:t^2T~bۚM)zײQD +6oٖ$ΦBb60aolvhh6Q.^PQSAyV d*$ 2 \ No newline at end of file diff --git a/tests/data/sign_template.xml b/tests/data/sign_template.xml new file mode 100644 index 00000000..f8523bc5 --- /dev/null +++ b/tests/data/sign_template.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/examples/base.py b/tests/examples/base.py deleted file mode 100644 index b846f5b1..00000000 --- a/tests/examples/base.py +++ /dev/null @@ -1,20 +0,0 @@ -from os import path -from lxml import etree - -BASE_DIR = path.dirname(__file__) - - -def parse_xml(name): - return etree.parse(path.join(BASE_DIR, name)).getroot() - - -def compare(name, result): - # Parse the expected file. - xml = parse_xml(name) - - # Stringify the root, nodes of the two documents. - expected_text = etree.tostring(xml, pretty_print=False) - result_text = etree.tostring(result, pretty_print=False) - - # Compare the results. - assert expected_text == result_text diff --git a/tests/examples/test_decrypt.py b/tests/examples/test_decrypt.py deleted file mode 100644 index b06771b6..00000000 --- a/tests/examples/test_decrypt.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import with_statement -from os import path -import xmlsec -from .base import parse_xml, BASE_DIR, compare - - -def read_from_file(filename): - with open(filename, "rb") as stream: - return stream.read() - - -def test_decrypt1(): - manager = xmlsec.KeysManager() - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_memory(read_from_file(filename), xmlsec.KeyFormat.PEM, None) - assert key is not None - manager.add_key(key) - - enc_ctx = xmlsec.EncryptionContext(manager) - - root = parse_xml("enc1-res.xml") - enc_data = xmlsec.tree.find_child(root, "EncryptedData", xmlsec.Namespace.ENC) - assert enc_data is not None - decrypted = enc_ctx.decrypt(enc_data) - assert decrypted.tag == "Data" - - compare("enc1-doc.xml", root) - - -def test_decrypt2(): - manager = xmlsec.KeysManager() - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_memory(read_from_file(filename), xmlsec.KeyFormat.PEM, None) - assert key is not None - manager.add_key(key) - - enc_ctx = xmlsec.EncryptionContext(manager) - - root = parse_xml("enc2-res.xml") - enc_data = xmlsec.tree.find_child(root, xmlsec.Node.ENCRYPTED_DATA, xmlsec.Namespace.ENC) - assert enc_data is not None - decrypted = enc_ctx.decrypt(enc_data) - assert decrypted.text == "\ntest\n" diff --git a/tests/examples/test_encrypt.py b/tests/examples/test_encrypt.py deleted file mode 100644 index d0fd0f9d..00000000 --- a/tests/examples/test_encrypt.py +++ /dev/null @@ -1,85 +0,0 @@ -from os import path -import xmlsec -from .base import parse_xml, BASE_DIR -from lxml import etree - - -def read_from_file(filename): - with open(filename, "rb") as stream: - return stream.read() - - -def test_encrypt_xml(): - # Load the public cert - manager = xmlsec.KeysManager() - filename = path.join(BASE_DIR, 'rsacert.pem') - key = xmlsec.Key.from_memory(read_from_file(filename), xmlsec.KeyFormat.CERT_PEM, None) - assert key is not None - manager.add_key(key) - template = parse_xml('enc1-doc.xml') - assert template is not None - # Prepare for encryption - enc_data = xmlsec.template.encrypted_data_create( - template, xmlsec.Transform.AES128, type=xmlsec.EncryptionType.ELEMENT, 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') - - assert data is not None - # Encrypt! - enc_ctx = xmlsec.EncryptionContext(manager) - enc_ctx.key = xmlsec.Key.generate(xmlsec.KeyData.AES, 128, xmlsec.KeyDataType.SESSION) - enc_datsa = enc_ctx.encrypt_xml(enc_data, data) - assert enc_data is not None - enc_method = xmlsec.tree.find_child(enc_data, xmlsec.Node.ENCRYPTION_METHOD, xmlsec.Namespace.ENC) - assert enc_method is not None - assert enc_method.get("Algorithm") == "http://www.w3.org/2001/04/xmlenc#aes128-cbc" - key_info = xmlsec.tree.find_child(enc_data, xmlsec.Node.KEY_INFO, xmlsec.Namespace.DS) - assert key_info is not None - enc_method = xmlsec.tree.find_node(key_info, xmlsec.Node.ENCRYPTION_METHOD, xmlsec.Namespace.ENC) - assert enc_method is not None - assert enc_method.get("Algorithm") == "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" - cipher_value = xmlsec.tree.find_node(key_info, xmlsec.Node.CIPHER_VALUE, xmlsec.Namespace.ENC) - assert cipher_value is not None - - -def test_encrypt_binary(): - # Load the public cert - manager = xmlsec.KeysManager() - filename = path.join(BASE_DIR, 'rsacert.pem') - key = xmlsec.Key.from_memory(read_from_file(filename), xmlsec.KeyFormat.CERT_PEM, None) - assert key is not None - manager.add_key(key) - template = etree.Element("root") - assert template is not None - # Prepare for encryption - enc_data = xmlsec.template.encrypted_data_create( - template, xmlsec.Transform.AES128, type=xmlsec.EncryptionType.CONTENT, ns="xenc", - mime_type="binary/octet-stream") - - 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) - - # Encrypt! - enc_ctx = xmlsec.EncryptionContext(manager) - enc_ctx.key = xmlsec.Key.generate(xmlsec.KeyData.AES, 128, xmlsec.KeyDataType.SESSION) - enc_data = enc_ctx.encrypt_binary(enc_data, b'test') - assert enc_data is not None - assert enc_data.tag == "{%s}%s" % (xmlsec.Namespace.ENC, xmlsec.Node.ENCRYPTED_DATA) - print(xmlsec.Node.ENCRYPTION_METHOD) - enc_method = xmlsec.tree.find_child(enc_data, xmlsec.Node.ENCRYPTION_METHOD, xmlsec.Namespace.ENC) - assert enc_method is not None - assert enc_method.get("Algorithm") == "http://www.w3.org/2001/04/xmlenc#aes128-cbc" - key_info = xmlsec.tree.find_child(enc_data, xmlsec.Node.KEY_INFO, xmlsec.Namespace.DS) - assert key_info is not None - enc_method = xmlsec.tree.find_node(key_info, xmlsec.Node.ENCRYPTION_METHOD, xmlsec.Namespace.ENC) - assert enc_method is not None - assert enc_method.get("Algorithm") == "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" - cipher_value = xmlsec.tree.find_node(key_info, xmlsec.Node.CIPHER_VALUE, xmlsec.Namespace.ENC) - assert cipher_value is not None diff --git a/tests/examples/test_sign.py b/tests/examples/test_sign.py deleted file mode 100644 index d873ba8c..00000000 --- a/tests/examples/test_sign.py +++ /dev/null @@ -1,307 +0,0 @@ -from os import path -import xmlsec -from .base import parse_xml, compare, BASE_DIR - - -def test_sign_template_pem(): - """ - Should sign a pre-constructed template file - using a key from a PEM file. - """ - - # Load the pre-constructed XML template. - template = parse_xml('sign1-tmpl.xml') - - # Find the node. - signature_node = xmlsec.tree.find_node(template, xmlsec.Node.SIGNATURE) - - assert signature_node is not None - assert signature_node.tag.endswith(xmlsec.Node.SIGNATURE) - - # Create a digital signature context (no key manager is needed). - ctx = xmlsec.SignatureContext() - - # Load private key (assuming that there is no password). - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_file(filename, xmlsec.KeyFormat.PEM) - - assert key is not None - - # Set key name to the file name (note: this is just a test). - key.name = path.basename(filename) - - # Set the key on the context. - ctx.key = key - - assert ctx.key is not None - assert ctx.key.name == path.basename(filename) - del key - - # Sign the template. - ctx.sign(signature_node) - - # Assert the contents of the XML document against the expected result. - compare('sign1-res.xml', template) - - -def test_sign_generated_template_pem(): - """ - Should sign a dynamicaly constructed template file - using a key from a PEM file. - """ - - # Load document file. - template = parse_xml('sign2-doc.xml') - - # Create a signature template for RSA-SHA1 enveloped signature. - signature_node = xmlsec.template.create( - template, - xmlsec.Transform.EXCL_C14N, - xmlsec.Transform.RSA_SHA1) - - assert signature_node is not None - - # Add the node to the document. - template.append(signature_node) - - # Add the node to the signature template. - ref = xmlsec.template.add_reference(signature_node, xmlsec.Transform.SHA1) - - # Add the enveloped transform descriptor. - xmlsec.template.add_transform(ref, xmlsec.Transform.ENVELOPED) - - # Add the and nodes. - key_info = xmlsec.template.ensure_key_info(signature_node) - xmlsec.template.add_key_name(key_info) - - # Create a digital signature context (no key manager is needed). - ctx = xmlsec.SignatureContext() - - # Load private key (assuming that there is no password). - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_file(filename, xmlsec.KeyFormat.PEM) - - assert key is not None - - # Set key name to the file name (note: this is just a test). - key.name = path.basename(filename) - - # Set the key on the context. - ctx.key = key - - assert ctx.key is not None - assert ctx.key.name == path.basename(filename) - - # Sign the template. - ctx.sign(signature_node) - - # Assert the contents of the XML document against the expected result. - compare('sign2-res.xml', template) - - -def test_sign_generated_template_pem_with_x509(): - """ - Should sign a file using a dynamicaly created template, key from PEM - file and an X509 certificate. - """ - - # Load document file. - template = parse_xml('sign3-doc.xml') - - # Create a signature template for RSA-SHA1 enveloped signature. - signature_node = xmlsec.template.create( - template, - xmlsec.Transform.EXCL_C14N, - xmlsec.Transform.RSA_SHA1) - - assert signature_node is not None - - # Add the node to the document. - template.append(signature_node) - - # Add the node to the signature template. - ref = xmlsec.template.add_reference(signature_node, xmlsec.Transform.SHA1) - - # Add the enveloped transform descriptor. - xmlsec.template.add_transform(ref, xmlsec.Transform.ENVELOPED) - - # Add the and nodes. - key_info = xmlsec.template.ensure_key_info(signature_node) - xmlsec.template.add_x509_data(key_info) - - # Create a digital signature context (no key manager is needed). - ctx = xmlsec.SignatureContext() - - # Load private key (assuming that there is no password). - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_file(filename, xmlsec.KeyFormat.PEM) - - assert key is not None - - # Load the certificate and add it to the key. - filename = path.join(BASE_DIR, 'rsacert.pem') - key.load_cert_from_file(filename, xmlsec.KeyFormat.PEM) - - # Set key name to the file name (note: this is just a test). - key.name = path.basename(filename) - - # Set the key on the context. - ctx.key = key - - assert ctx.key is not None - assert ctx.key.name == path.basename(filename) - - # Sign the template. - ctx.sign(signature_node) - - # Assert the contents of the XML document against the expected result. - compare('sign3-res.xml', template) - - -def test_sign_generated_template_pem_with_x509_with_custom_ns(): - """ - Should sign a file using a dynamicaly created template, key from PEM - file and an X509 certificate with custom ns. - """ - - # Load document file. - template = parse_xml('sign4-doc.xml') - xmlsec.tree.add_ids(template, ["ID"]) - elem_id = template.get('ID', None) - if elem_id: - elem_id = '#' + elem_id - # Create a signature template for RSA-SHA1 enveloped signature. - signature_node = xmlsec.template.create( - template, - xmlsec.Transform.EXCL_C14N, - xmlsec.Transform.RSA_SHA1, ns='ds') - - assert signature_node is not None - - # Add the node to the document. - template.append(signature_node) - - # Add the node to the signature template. - ref = xmlsec.template.add_reference(signature_node, xmlsec.Transform.SHA1, uri=elem_id) - - # Add the enveloped transform descriptor. - xmlsec.template.add_transform(ref, xmlsec.Transform.ENVELOPED) - # Add the excl_c14n transform descriptor. - xmlsec.template.add_transform(ref, xmlsec.Transform.EXCL_C14N) - - # Add the and nodes. - key_info = xmlsec.template.ensure_key_info(signature_node) - xmlsec.template.add_x509_data(key_info) - - # Create a digital signature context (no key manager is needed). - ctx = xmlsec.SignatureContext() - - # Load private key (assuming that there is no password). - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_file(filename, xmlsec.KeyFormat.PEM) - - assert key is not None - - # Load the certificate and add it to the key. - filename = path.join(BASE_DIR, 'rsacert.pem') - key.load_cert_from_file(filename, xmlsec.KeyFormat.PEM) - - # Set key name to the file name (note: this is just a test). - key.name = path.basename(filename) - - # Set the key on the context. - ctx.key = key - - assert ctx.key is not None - assert ctx.key.name == path.basename(filename) - - # Sign the template. - ctx.sign(signature_node) - - # Assert the contents of the XML document against the expected result. - compare('sign4-res.xml', template) - - -def test_sign_generated_template_pem_with_x509_with_cert_info(): - """ - Should sign a file using a dynamicaly created template, key from PEM - file and an X509 certificate. - """ - - # Load document file. - template = parse_xml('sign5-doc.xml') - - # Create a signature template for RSA-SHA1 enveloped signature. - signature_node = xmlsec.template.create( - template, - xmlsec.Transform.EXCL_C14N, - xmlsec.Transform.RSA_SHA1) - - assert signature_node is not None - - # Add the node to the document. - template.append(signature_node) - - # Add the node to the signature template. - ref = xmlsec.template.add_reference(signature_node, xmlsec.Transform.SHA1) - - # Add the enveloped transform descriptor. - xmlsec.template.add_transform(ref, xmlsec.Transform.ENVELOPED) - - # Add the and nodes. - key_info = xmlsec.template.ensure_key_info(signature_node) - x509_data = xmlsec.template.add_x509_data(key_info) - xmlsec.template.x509_data_add_subject_name(x509_data) - xmlsec.template.x509_data_add_certificate(x509_data) - xmlsec.template.x509_data_add_ski(x509_data) - x509_issuer_serial = xmlsec.template.x509_data_add_issuer_serial(x509_data) - xmlsec.template.x509_issuer_serial_add_issuer_name(x509_issuer_serial, 'Test Issuer') - xmlsec.template.x509_issuer_serial_add_serial_number(x509_issuer_serial, '1') - - # Create a digital signature context (no key manager is needed). - ctx = xmlsec.SignatureContext() - - # Load private key (assuming that there is no password). - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_file(filename, xmlsec.KeyFormat.PEM) - - assert key is not None - - # Load the certificate and add it to the key. - filename = path.join(BASE_DIR, 'rsacert.pem') - key.load_cert_from_file(filename, xmlsec.KeyFormat.PEM) - - # Set key name to the file name (note: this is just a test). - key.name = path.basename(filename) - - # Set the key on the context. - ctx.key = key - - assert ctx.key is not None - assert ctx.key.name == path.basename(filename) - - # Sign the template. - ctx.sign(signature_node) - - # Assert the contents of the XML document against the expected result. - compare('sign5-res.xml', template) - - -def test_sign_binary(): - ctx = xmlsec.SignatureContext() - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_file(filename, xmlsec.KeyFormat.PEM) - assert key is not None - - key.name = path.basename(filename) - - # Set the key on the context. - ctx.key = key - - assert ctx.key is not None - assert ctx.key.name == path.basename(filename) - - 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' - expected_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" - sign = ctx.sign_binary(data, xmlsec.Transform.RSA_SHA1) - assert sign == expected_sign diff --git a/tests/examples/test_verify.py b/tests/examples/test_verify.py deleted file mode 100644 index 7d8c2054..00000000 --- a/tests/examples/test_verify.py +++ /dev/null @@ -1,82 +0,0 @@ -from os import path -from pytest import mark -import xmlsec -from .base import parse_xml, BASE_DIR - - -@mark.parametrize('index', range(1, 5)) -def test_verify_with_pem_file(index): - """Should verify a signed file using a key from a PEM file. - """ - - # Load the XML document. - template = parse_xml('sign%d-res.xml' % index) - xmlsec.tree.add_ids(template, ["ID"]) - # Find the node. - signature_node = xmlsec.tree.find_node(template, xmlsec.Node.SIGNATURE) - - assert signature_node is not None - assert signature_node.tag.endswith(xmlsec.Node.SIGNATURE) - - # Create a digital signature context (no key manager is needed). - ctx = xmlsec.SignatureContext() - - # Load the public key. - filename = path.join(BASE_DIR, 'rsapub.pem') - key = xmlsec.Key.from_file(filename, xmlsec.KeyFormat.PEM) - - assert key is not None - - # Set key name to the file name (note: this is just a test). - key.name = path.basename(filename) - - # Set the key on the context. - ctx.key = key - - assert ctx.key is not None - assert ctx.key.name == path.basename(filename) - - # Verify the signature. - ctx.verify(signature_node) - - -def test_validate_binary_sign(): - ctx = xmlsec.SignatureContext() - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_file(filename, xmlsec.KeyFormat.PEM) - assert key is not None - - key.name = path.basename(filename) - - # Set the key on the context. - ctx.key = key - - assert ctx.key is not None - assert ctx.key.name == path.basename(filename) - - 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.Transform.RSA_SHA1, sign) - - -def test_validate_binary_sign_fail(): - ctx = xmlsec.SignatureContext() - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_file(filename, xmlsec.KeyFormat.PEM) - assert key is not None - - key.name = path.basename(filename) - - # Set the key on the context. - ctx.key = key - - assert ctx.key is not None - assert ctx.key.name == path.basename(filename) - - 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"invalid" - try: - ctx.verify_binary(data, xmlsec.Transform.RSA_SHA1, sign) - assert False - except xmlsec.Error: - pass 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 new file mode 100644 index 00000000..dd0657d3 --- /dev/null +++ b/tests/test_ds.py @@ -0,0 +1,337 @@ +import unittest + +import xmlsec +from tests import base + +consts = xmlsec.constants + + +class TestSignContext(base.TestMemoryLeaks): + def test_init(self): + ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) + del ctx + + 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) + + 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') + + 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') + 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) + 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) + + 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') + sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1) + self.assertIsNotNone(sign) + root.append(sign) + ref = xmlsec.template.add_reference(sign, consts.TransformSha1) + xmlsec.template.add_transform(ref, consts.TransformEnveloped) + ki = xmlsec.template.ensure_key_info(sign) + xmlsec.template.add_key_name(ki) + + ctx = xmlsec.SignatureContext() + 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) + + ctx.sign(sign) + 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') + sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1) + self.assertIsNotNone(sign) + root.append(sign) + ref = xmlsec.template.add_reference(sign, consts.TransformSha1) + xmlsec.template.add_transform(ref, consts.TransformEnveloped) + ki = xmlsec.template.ensure_key_info(sign) + xmlsec.template.add_x509_data(ki) + + ctx = xmlsec.SignatureContext() + 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) + + ctx.sign(sign) + 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']) + elem_id = root.get('ID', None) + if elem_id: + elem_id = '#' + elem_id + 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) + xmlsec.template.add_transform(ref, consts.TransformEnveloped) + xmlsec.template.add_transform(ref, consts.TransformExclC14N) + ki = xmlsec.template.ensure_key_info(sign) + xmlsec.template.add_x509_data(ki) + + ctx = xmlsec.SignatureContext() + 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) + + ctx.sign(sign) + 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') + sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1) + self.assertIsNotNone(sign) + root.append(sign) + ref = xmlsec.template.add_reference(sign, consts.TransformSha1) + xmlsec.template.add_transform(ref, consts.TransformEnveloped) + + ki = xmlsec.template.ensure_key_info(sign) + x509 = xmlsec.template.add_x509_data(ki) + xmlsec.template.x509_data_add_subject_name(x509) + xmlsec.template.x509_data_add_certificate(x509) + xmlsec.template.x509_data_add_ski(x509) + x509_issuer_serial = xmlsec.template.x509_data_add_issuer_serial(x509) + xmlsec.template.x509_issuer_serial_add_issuer_name(x509_issuer_serial, 'Test Issuer') + 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) + 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) + + ctx.sign(sign) + 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) + self.assertIsNotNone(ctx.key) + ctx.key.name = 'rsakey.pem' + 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) + + 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) + + def test_verify_case_2(self): + self.check_verify(2) + + def test_verify_case_3(self): + self.check_verify(3) + + def test_verify_case_4(self): + self.check_verify(4) + + def test_verify_case_5(self): + self.check_verify(5) + + def check_verify(self, i): + 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]) + + ctx = xmlsec.SignatureContext() + 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) + 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) + self.assertIsNotNone(ctx.key) + ctx.key.name = 'rsakey.pem' + self.assertEqual('rsakey.pem', ctx.key.name) + + 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) + self.assertIsNotNone(ctx.key) + ctx.key.name = 'rsakey.pem' + self.assertEqual('rsakey.pem', ctx.key.name) + with self.assertRaises(xmlsec.Error): + 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 new file mode 100644 index 00000000..41f78d74 --- /dev/null +++ b/tests/test_enc.py @@ -0,0 +1,235 @@ +import tempfile + +from lxml import etree + +import xmlsec +from tests import base + +consts = xmlsec.constants + + +class TestEncryptionContext(base.TestMemoryLeaks): + def test_init(self): + ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) + del ctx + + 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) + + 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') + 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) + data = root.find('./Data') + self.assertIsNotNone(data) + + 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) + + encrypted = ctx.encrypt_xml(enc_data, data) + self.assertIsNotNone(encrypted) + + 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_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' + ) + 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) + + encrypted = ctx.encrypt_binary(enc_data, b'test') + 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_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) + + def test_decrypt2(self): + self.check_decrypt(2) + + def test_decrypt_key(self): + root = self.load_xml('enc3-out.xml') + enc_key = xmlsec.tree.find_child(root, consts.NodeEncryptedKey, consts.EncNs) + self.assertIsNotNone(enc_key) + + manager = xmlsec.KeysManager() + 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() + root.remove(enc_key) + ctx.key = xmlsec.Key.from_binary_data(consts.KeyDataAes, keydata) + enc_data = xmlsec.tree.find_child(root, consts.NodeEncryptedData, consts.EncNs) + self.assertIsNotNone(enc_data) + decrypted = ctx.decrypt(enc_data) + self.assertIsNotNone(decrypted) + self.assertEqual(self.load_xml('enc3-in.xml'), decrypted) + + def check_decrypt(self, 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)) + ctx = xmlsec.EncryptionContext(manager) + decrypted = ctx.decrypt(enc_data) + self.assertIsNotNone(decrypted) + self.assertEqual(self.load_xml(f'enc{i}-in.xml'), root) + + 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 new file mode 100644 index 00000000..977ddf82 --- /dev/null +++ b/tests/test_keys.py @@ -0,0 +1,220 @@ +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) + self.assertIsNotNone(key) + + def test_key_from_memory_with_bad_args(self): + with self.assertRaises(TypeError): + 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) + self.assertIsNotNone(key) + + def test_key_from_file_with_bad_args(self): + with self.assertRaises(TypeError): + 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: + 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='') + + 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')) + self.assertIsNotNone(key) + + def test_from_binary_file_with_bad_args(self): + with self.assertRaises(TypeError): + 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')) + self.assertIsNotNone(key) + + def test_from_binary_data_with_bad_args(self): + with self.assertRaises(TypeError): + 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) + self.assertIsNotNone(key) + 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) + self.assertIsNotNone(key) + with self.assertRaises(TypeError): + 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) + self.assertIsNotNone(key) + 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) + self.assertIsNotNone(key) + 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) + self.assertIsNotNone(key) + with self.assertRaises(TypeError): + 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_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) + + 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) + key2 = copy.copy(key) + del key + 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) + 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('') + + 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) + + 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)) + 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='') + + 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) + + 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)) + with self.assertRaises(TypeError): + 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() + with self.assertRaises(ValueError): + mngr.add_key(xmlsec.Key()) 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 new file mode 100644 index 00000000..bbf7f42d --- /dev/null +++ b/tests/test_templates.py @@ -0,0 +1,213 @@ +import unittest + +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') + sign = xmlsec.template.create( + root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1, id='Id', ns='test' + ) + 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') + enc = xmlsec.template.encrypted_data_create( + root, method=consts.TransformDes3Cbc, id='Id', type='Type', mime_type='MimeType', encoding='Encoding', ns='test' + ) + for a in ('Id', 'Type', 'MimeType', 'Encoding'): + self.assertEqual(a, enc.get(a)) + self.assertEqual('test', enc.prefix) + + def test_ensure_key_info(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, 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') + 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(a, ek2.get(a)) + + def test_add_key_name(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) + 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) + + 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') + 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'): + 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') + 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)) + + 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') + 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) + xmlsec.template.x509_data_add_certificate(x509) + xmlsec.template.x509_data_add_crl(x509) + issuer = xmlsec.template.x509_data_add_issuer_serial(x509) + xmlsec.template.x509_data_add_ski(x509) + 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)) + + 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') + 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) + + 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') + 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)) + + 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') + 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) + + 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') + 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') + 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)) + + 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 new file mode 100644 index 00000000..5e80a60a --- /dev/null +++ b/tests/test_tree.py @@ -0,0 +1,45 @@ +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') + 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') + 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') + 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']) + + 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()