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 27f72d21..15f47985 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,10 @@ .* !.editorconfig !.travis* +!.appveyor* !.git* +!.readthedocs.yaml +!.pre-commit-config.yaml # Python /dist @@ -11,4 +14,3 @@ *.pyo *.egg* *.so -*.c 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 3bc78c85..8d3ca07e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,53 @@ +dist: focal language: python +travis: + auto_cancel: + push: true + pull_request: true + +notifications: + email: false + python: - - '2.7' - - '3.3' - - '3.4' + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + +env: + global: + - CFLAGS=-coverage + - LDFLAGS=-coverage -lgcov + - PYXMLSEC_TEST_ITERATIONS=50 -before_install: - - 'travis_retry sudo apt-get update' - - 'travis_retry sudo apt-get install --fix-missing -y python-dev libxslt1-dev libssl-dev' - - 'travis_retry pip install Cython --use-mirrors' - - 'travis_retry wget -O /dev/stdout https://www.aleksey.com/xmlsec/download/xmlsec1-1.2.20.tar.gz | tar xzv' - - 'cd xmlsec1-1.2.20 && ./configure && make && sudo make install && sudo ldconfig && cd ..' +addons: + apt: + packages: + - libssl-dev + - libxmlsec1 + - libxmlsec1-dev + - libxmlsec1-openssl + - libxslt1-dev + - pkg-config + - lcov install: - - 'travis_retry pip install -e ".[test]" --use-mirrors' + - 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 -script: 'py.test' +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 941c90f1..6c47dc9c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,14 @@ -include src/*.h -include src/xmlsec/*.pxd +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 index a5a4a18d..60bde880 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,131 @@ # python-xmlsec -[![Build Status](https://travis-ci.org/mehcode/python-xmlsec.png?branch=master)](https://travis-ci.org/mehcode/python-xmlsec) -[![PyPi Version](https://pypip.in/v/xmlsec/badge.png)](https://pypi.python.org/pypi/xmlsec) -![PyPi Downloads](https://pypip.in/d/xmlsec/badge.png) -> Python bindings for the XML Security Library. + +[![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://github.com/mehcode/python-xmlsec/tree/master/tests/examples) to see various examples of signing and verifying using the library. +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 -### Pre-Install +`xmlsec` is available on PyPI: -#### Linux (Debian) +``` bash +pip install xmlsec +``` - ```sh - apt-get install libxml2-dev libxmlsec1-dev - ``` - -#### Linux (CentOS) +Depending on your OS, you may need to install the required native +libraries first: - ```sh - yum install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel - ``` +### Linux (Debian) -#### Mac +``` bash +apt-get install pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl +``` - ```sh - brew install libxml2 libxmlsec1 - ``` +Note: There is no required version of LibXML2 for Ubuntu Precise, so you +need to download and install it manually. -### Automated +``` 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 +``` -1. **xmlsec** can be installed through `easy_install` or `pip`. +### Linux (CentOS) - ```sh - pip install xmlsec - ``` +``` bash +yum install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel +``` -#### Mac +### Linux (Fedora) -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. +``` bash +dnf install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel +``` -### Manual +### Mac -1. Clone the **xmlsec** repository to your local computer. +``` bash +brew install libxml2 libxmlsec1 pkg-config +``` - ```sh - git clone git://github.com/mehcode/python-xmlsec.git - ``` +or -2. Change into the **xmlsec** root directory. +``` bash +port install libxml2 xmlsec pkgconfig +``` - ```sh - cd /path/to/xmlsec - ``` +### 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`. - ```sh + ``` bash pip install . ``` @@ -67,26 +133,33 @@ include the appropriate files from the libxml2 and libxmlsec1 libraries. ### Setting up your environment -1. Follow steps 1 and 2 of the [manual installation instructions][]. +1. Follow steps 1 and 2 of the [manual installation + instructions](#building-from-source). -[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-identical + versions of packages. -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. - - ```sh + ``` 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 + 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: -3. Install **xmlsec** in development mode with testing enabled. - This will download all dependencies required for running the unit tests. + ``` bash + workon xmlsec + ``` + +4. Install `xmlsec` in development mode with testing enabled. This will + download all dependencies required for running the unit tests. - ```sh - pip install -e ".[test]" + ``` bash + pip install -r requirements-test.txt + pip install -e "." ``` ### Running the test suite @@ -95,12 +168,31 @@ include the appropriate files from the libxml2 and libxmlsec1 libraries. 2. Run the unit tests. - ```sh - py.test + ``` bash + pytest tests ``` -## License +3. Tests configuration + + Env variable `PYXMLSEC_TEST_ITERATIONS` specifies number of test + iterations to detect memory leaks. + +### Reporting an issue -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. +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 -[opensource.org]: http://opensource.org/licenses/MIT +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/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 new file mode 100644 index 00000000..70fe9703 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,5 @@ +-r requirements.txt + +pytest==8.4.1 +lxml-stubs==0.5.1 +ruff[format]==0.14.4 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..8221c374 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +lxml==6.0.2 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0a2ab086..00000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[bdist_rpm] -release = 1 -build-requires = pkgconfig xmlsec1-devel libxml2-devel xmlsec1-openssl-devel -group = Development/Libraries -requires = xmlsec1 xmlsec1-openssl \ No newline at end of file diff --git a/setup.py b/setup.py index 43c6074a..946855f1 100644 --- a/setup.py +++ b/setup.py @@ -1,123 +1,15 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- -# from __future__ import absolute_import, unicode_literals, division -from os import path -from pkgutil import get_importer -from setuptools import setup, Extension -from functools import wraps +from pathlib import Path +from setuptools import Extension, setup -def lazy(function): +from build_support.build_ext import build_ext - @wraps(function) - def wrapped(*args, **kwargs): - - class LazyProxy(Extension): - __arguments = dict() - - def __init__(self, function, args, kwargs): - self.__arguments["function"] = function - self.__arguments["args"] = args - self.__arguments["kwargs"] = kwargs - self.__arguments["result"] = None - - def __getattr__(self, item): - if self.__arguments["result"] is None: - self.__arguments["result"] = self.__arguments["function"](*self.__arguments["args"], - **self.__arguments["kwargs"]) - - return getattr(self.__arguments["result"], item) - - def __setattr__(self, name, value): - if self.__arguments["result"] is None: - self.__arguments["result"] = self.__arguments["function"](*self.__arguments["args"], - **self.__arguments["kwargs"]) - - setattr(self.__arguments["result"], name, value) - - return LazyProxy(function, args, kwargs) - - return wrapped - - -@lazy -def make_extension(name, cython=True): - from pkgconfig import parse - - # Declare the crypto implementation. - xmlsec_crypto = 'openssl' - - # Process the `pkg-config` utility and discover include and library - # directories. - config = {} - for lib in ['libxml-2.0', 'xmlsec1-%s' % xmlsec_crypto]: - config.update(parse(lib)) - - config['extra_compile_args'] = ['-DXMLSEC_CRYPTO_OPENSSL=1'] - - # List-ify config for setuptools. - for key in config: - config[key] = list(config[key]) - - if 'include_dirs' not in config: - config['include_dirs'] = [] - - # Add the source directories for inclusion. - import lxml - config['include_dirs'].insert(0, path.dirname(lxml.__file__)) - config['include_dirs'].insert(0, path.join(path.dirname(lxml.__file__), 'includes')) - config['include_dirs'].insert(0, 'src') - - # Resolve extension location from name. - location = path.join('src', *name.split('.')) - location += '.pyx' if cython else '.c' - - # Create and return the extension. - return Extension(name, [location], **config) - - -# Navigate, import, and retrieve the metadata of the project. -meta = get_importer('src/xmlsec').find_module('meta').load_module('meta') +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) setup( - name='xmlsec', - version=meta.version, - description=meta.description, - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Cython', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', - 'Topic :: Text Processing :: Markup :: XML' - ], - author='Ryan Leckey', - author_email='support@mehcode.com', - url='https://github.com/mehcode/python-xmlsec', - setup_requires=[ - 'setuptools_cython', - 'pkgconfig', - 'lxml >= 3.0', - ], - install_requires=[ - 'lxml >= 3.0', - ], - extras_require={ - 'test': ['pytest'] - }, - package_dir={'xmlsec': 'src/xmlsec'}, - packages=['xmlsec'], - ext_modules=[ - make_extension('xmlsec.constants'), - make_extension('xmlsec.utils'), - make_extension('xmlsec.tree'), - make_extension('xmlsec.key'), - make_extension('xmlsec.ds'), - make_extension('xmlsec.enc'), - make_extension('xmlsec.template'), - ] + ext_modules=[pyxmlsec], + cmdclass={'build_ext': build_ext}, ) diff --git a/src/common.h b/src/common.h new file mode 100644 index 00000000..a6176551 --- /dev/null +++ b/src/common.h @@ -0,0 +1,26 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __PYXMLSEC_COMMON_H__ +#define __PYXMLSEC_COMMON_H__ + +#include "debug.h" + +#ifndef MODULE_NAME +#define MODULE_NAME xmlsec +#endif + +#define JOIN(X,Y) DO_JOIN1(X,Y) +#define DO_JOIN1(X,Y) DO_JOIN2(X,Y) +#define DO_JOIN2(X,Y) X##Y + +#define DO_STRINGIFY(x) #x +#define STRINGIFY(x) DO_STRINGIFY(x) + +#endif //__PYXMLSEC_COMMON_H__ diff --git a/src/constants.c b/src/constants.c new file mode 100644 index 00000000..bd1fa5e0 --- /dev/null +++ b/src/constants.c @@ -0,0 +1,585 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "common.h" +#include "constants.h" + +#define PYXMLSEC_CONSTANTS_DOC "Various constants used by the library.\n" + +// destructor +static void PyXmlSec_Transform__del__(PyObject* self) { + PYXMLSEC_DEBUGF("%p", self); + Py_TYPE(self)->tp_free(self); +} + +// __str__ method +static PyObject* PyXmlSec_Transform__str__(PyObject* self) { + char buf[300]; + PyXmlSec_Transform* transform = (PyXmlSec_Transform*)(self); + 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 PyUnicode_FromString((const char*)self->id->name); +} + +static const char PyXmlSec_TransformHrefGet__doc__[] = "The transform's identification string (href)."; +static PyObject* PyXmlSec_TransformHrefGet(PyXmlSec_Transform* self, void* closure) { + if (self->id->href != NULL) + return PyUnicode_FromString((const char*)self->id->href); + Py_RETURN_NONE; +} + +static const char PyXmlSec_TransformUsageGet__doc__[] = "The allowed transforms usages."; +static PyObject* PyXmlSec_TransformUsageGet(PyXmlSec_Transform* self, void* closure) { + return PyLong_FromUnsignedLong(self->id->usage); +} + +static PyGetSetDef PyXmlSec_TransformGetSet[] = { + { + "name", + (getter)PyXmlSec_TransformNameGet, + NULL, + (char*)PyXmlSec_TransformNameGet__doc__, + NULL + }, + { + "href", + (getter)PyXmlSec_TransformHrefGet, + NULL, + (char*)PyXmlSec_TransformHrefGet__doc__, + NULL + }, + { + "usage", + (getter)PyXmlSec_TransformUsageGet, + NULL, + (char*)PyXmlSec_TransformUsageGet__doc__, + NULL + }, + {NULL} /* Sentinel */ +}; + +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 */ + 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; + +static PyObject* PyXmlSec_TransformNew(xmlSecTransformId id) { + PyXmlSec_Transform* transform = PyObject_New(PyXmlSec_Transform, PyXmlSec_TransformType); + if (transform != NULL) { + transform->id = id; + } + return (PyObject*)transform; +} + +// destructor +static void PyXmlSec_KeyData__del__(PyObject* self) { + PYXMLSEC_DEBUGF("%p", self); + Py_TYPE(self)->tp_free(self); +} + +// __str__ method +static PyObject* PyXmlSec_KeyData__str__(PyObject* self) { + char buf[300]; + PyXmlSec_KeyData* keydata = (PyXmlSec_KeyData*)(self); + 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 PyUnicode_FromString((const char*)self->id->name); +} + +static const char PyXmlSec_KeyDataHrefGet__doc__[] = "The key data's identification string (href)."; +static PyObject* PyXmlSec_KeyDataHrefGet(PyXmlSec_KeyData* self, void* closure) { + if (self->id->href != NULL) + return PyUnicode_FromString((const char*)self->id->href); + Py_RETURN_NONE; +} + +static PyGetSetDef PyXmlSec_KeyDataGetSet[] = { + { + "name", + (getter)PyXmlSec_KeyDataNameGet, + NULL, + (char*)PyXmlSec_KeyDataNameGet__doc__, + NULL + }, + { + "href", + (getter)PyXmlSec_KeyDataHrefGet, + NULL, + (char*)PyXmlSec_KeyDataHrefGet__doc__, + NULL + }, + {NULL} /* Sentinel */ +}; + +static PyTypeObject _PyXmlSec_KeyDataType = { + PyVarObject_HEAD_INIT(NULL, 0) + STRINGIFY(MODULE_NAME) ".constants.__KeyData", /* tp_name */ + sizeof(PyXmlSec_KeyData), /* tp_basicsize */ + 0, /* tp_itemsize */ + PyXmlSec_KeyData__del__, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved */ + PyXmlSec_KeyData__repr__, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + PyXmlSec_KeyData__str__, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "The xmlSecKeyDataId 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_KeyDataGetSet, /* 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_KeyDataType = &_PyXmlSec_KeyDataType; + +static PyObject* PyXmlSec_KeyDataNew(xmlSecKeyDataId id) { + PyXmlSec_KeyData* keydata = PyObject_New(PyXmlSec_KeyData, PyXmlSec_KeyDataType); + if (keydata != NULL) { + keydata->id = id; + } + return (PyObject*)keydata; +} + +static PyModuleDef PyXmlSec_ConstantsModule = +{ + PyModuleDef_HEAD_INIT, + STRINGIFY(MODULE_NAME) ".constants", + PYXMLSEC_CONSTANTS_DOC, + -1, NULL, NULL, NULL, NULL, NULL +}; + +// initialize constants module and registers it base package +int PyXmlSec_ConstantsModule_Init(PyObject* package) { + PyObject* constants = NULL; + PyObject* nsCls = NULL; + PyObject* nodeCls = NULL; + PyObject* transformCls = NULL; + PyObject* encryptionTypeCls = NULL; + PyObject* keyFormatCls = NULL; + PyObject* keyDataCls = NULL; + PyObject* keyDataTypeCls = NULL; + PyObject* tmp = NULL; + + constants = PyModule_Create(&PyXmlSec_ConstantsModule); + + if (!constants) return -1; + + if (PyType_Ready(PyXmlSec_TransformType) < 0) goto ON_FAIL; + if (PyType_Ready(PyXmlSec_KeyDataType) < 0) goto ON_FAIL; + +#define PYXMLSEC_ADD_INT_CONSTANT(name) PyModule_AddIntConstant(constants, STRINGIFY(name), JOIN(xmlSec, name)) + + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUsageUnknown) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUsageDSigTransform) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUsageC14NMethod) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUsageDigestMethod) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUsageSignatureMethod) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUsageEncryptionMethod) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUsageAny) < 0) goto ON_FAIL; + +#undef PYXMLSEC_ADD_INT_CONSTANT + +#define PYXMLSEC_DECLARE_NAMESPACE(var, name) \ + 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 + +#define PYXMLSEC_CLOSE_NAMESPACE(var) \ + Py_DECREF(var); var = NULL // compensate add ref from declare namespace + +#define PYXMLSEC_ADD_CONSTANT(ns, name, lname) \ + if (tmp == NULL) goto ON_FAIL; \ + if (PyModule_AddObject(constants, STRINGIFY(name), tmp) < 0) goto ON_FAIL; \ + Py_INCREF(tmp); \ + if (PyModule_AddObject(ns, lname, tmp) < 0) goto ON_FAIL; \ + tmp = NULL; + + +#define PYXMLSEC_ADD_NS_CONSTANT(name, lname) \ + tmp = PyUnicode_FromString((const char*)(JOIN(xmlSec, name))); \ + PYXMLSEC_ADD_CONSTANT(nsCls, name, lname); + + // namespaces + PYXMLSEC_DECLARE_NAMESPACE(nsCls, "Namespace"); + + PYXMLSEC_ADD_NS_CONSTANT(Ns, "BASE"); + PYXMLSEC_ADD_NS_CONSTANT(DSigNs, "DS"); + PYXMLSEC_ADD_NS_CONSTANT(EncNs, "ENC"); +#ifndef XMLSEC_NO_XKMS + PYXMLSEC_ADD_NS_CONSTANT(XkmsNs, "XKMS"); +#endif + PYXMLSEC_ADD_NS_CONSTANT(XPathNs, "XPATH"); + PYXMLSEC_ADD_NS_CONSTANT(XPath2Ns, "XPATH2"); + PYXMLSEC_ADD_NS_CONSTANT(XPointerNs, "XPOINTER"); + PYXMLSEC_ADD_NS_CONSTANT(NsExcC14N, "EXC_C14N"); + PYXMLSEC_ADD_NS_CONSTANT(NsExcC14NWithComments, "EXC_C14N_WITH_COMMENT"); + + PYXMLSEC_CLOSE_NAMESPACE(nsCls); + +#undef PYXMLSEC_ADD_NS_CONSTANT + + +#define PYXMLSEC_ADD_ENC_CONSTANT(name, lname) \ + tmp = PyUnicode_FromString((const char*)(JOIN(xmlSec, name))); \ + PYXMLSEC_ADD_CONSTANT(encryptionTypeCls, name, lname); + + // encryption type + PYXMLSEC_DECLARE_NAMESPACE(encryptionTypeCls, "EncryptionType"); + + PYXMLSEC_ADD_ENC_CONSTANT(TypeEncContent, "CONTENT"); + PYXMLSEC_ADD_ENC_CONSTANT(TypeEncElement, "ELEMENT"); + + PYXMLSEC_CLOSE_NAMESPACE(encryptionTypeCls); + +#undef PYXMLSEC_ADD_ENC_CONSTANT + + +#define PYXMLSEC_ADD_NODE_CONSTANT(name, lname) \ + tmp = PyUnicode_FromString((const char*)(JOIN(xmlSec, name))); \ + PYXMLSEC_ADD_CONSTANT(nodeCls, name, lname); + + // node + PYXMLSEC_DECLARE_NAMESPACE(nodeCls, "Node"); + + PYXMLSEC_ADD_NODE_CONSTANT(NodeSignature, "SIGNATURE"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeSignedInfo, "SIGNED_INFO"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeCanonicalizationMethod, "CANONICALIZATION_METHOD"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeSignatureMethod, "SIGNATURE_METHOD"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeSignatureValue, "SIGNATURE_VALUE"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeSignatureProperties, "SIGNATURE_PROPERTIES"); + + PYXMLSEC_ADD_NODE_CONSTANT(NodeDigestMethod, "DIGEST_METHOD"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeDigestValue, "DIGEST_VALUE"); + + PYXMLSEC_ADD_NODE_CONSTANT(NodeObject, "OBJECT"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeManifest, "MANIFEST"); + + PYXMLSEC_ADD_NODE_CONSTANT(NodeEncryptedData, "ENCRYPTED_DATA"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeEncryptedKey, "ENCRYPTED_KEY"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeEncryptionMethod, "ENCRYPTION_METHOD"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeEncryptionProperty, "ENCRYPTION_PROPERTY"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeEncryptionProperties, "ENCRYPTION_PROPERTIES"); + + PYXMLSEC_ADD_NODE_CONSTANT(NodeCipherData, "CIPHER_DATA"); + PYXMLSEC_ADD_NODE_CONSTANT(NodeCipherValue, "CIPHER_VALUE"); + + PYXMLSEC_ADD_NODE_CONSTANT(NodeCipherReference, "CIPHER_REFERENCE"); + 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 + + +#define PYXMLSEC_ADD_KEY_FORMAT_CONSTANT(name, lname) \ + tmp = PyLong_FromUnsignedLong((unsigned long)(JOIN(xmlSec, name))); \ + PYXMLSEC_ADD_CONSTANT(keyFormatCls, name, lname); + + // key format + PYXMLSEC_DECLARE_NAMESPACE(keyFormatCls, "KeyFormat"); + + PYXMLSEC_ADD_KEY_FORMAT_CONSTANT(KeyDataFormatUnknown, "UNKNOWN"); + PYXMLSEC_ADD_KEY_FORMAT_CONSTANT(KeyDataFormatBinary, "BINARY"); + PYXMLSEC_ADD_KEY_FORMAT_CONSTANT(KeyDataFormatPem, "PEM"); + PYXMLSEC_ADD_KEY_FORMAT_CONSTANT(KeyDataFormatDer, "DER"); + PYXMLSEC_ADD_KEY_FORMAT_CONSTANT(KeyDataFormatPkcs8Pem, "PKCS8_PEM"); + PYXMLSEC_ADD_KEY_FORMAT_CONSTANT(KeyDataFormatPkcs8Der, "PKCS8_DER");; + PYXMLSEC_ADD_KEY_FORMAT_CONSTANT(KeyDataFormatPkcs12, "PKCS12_PEM"); + PYXMLSEC_ADD_KEY_FORMAT_CONSTANT(KeyDataFormatCertPem, "CERT_PEM"); + PYXMLSEC_ADD_KEY_FORMAT_CONSTANT(KeyDataFormatCertDer, "CERT_DER"); + + PYXMLSEC_CLOSE_NAMESPACE(keyFormatCls); +#undef PYXMLSEC_ADD_KEY_FORMAT_CONSTANT + + +#define PYXMLSEC_ADD_KEY_TYPE_CONSTANT(name, lname) \ + tmp = PyLong_FromUnsignedLong((unsigned long)(JOIN(xmlSec, name))); \ + PYXMLSEC_ADD_CONSTANT(keyDataTypeCls, name, lname); + + // key data type + PYXMLSEC_DECLARE_NAMESPACE(keyDataTypeCls, "KeyDataType"); + + PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypeUnknown, "UNKNOWN"); + PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypeNone, "NONE"); + 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(KeyDataTypePermanent, "PERMANENT"); + PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypeTrusted, "TRUSTED"); + PYXMLSEC_ADD_KEY_TYPE_CONSTANT(KeyDataTypeAny, "ANY"); + + PYXMLSEC_CLOSE_NAMESPACE(keyDataTypeCls); +#undef PYXMLSEC_ADD_KEY_TYPE_CONSTANT + + +#define PYXMLSEC_ADD_KEYDATA_CONSTANT(name, lname) \ + tmp = PyXmlSec_KeyDataNew(xmlSec ## name ## Id); \ + PYXMLSEC_ADD_CONSTANT(keyDataCls, name, lname); + + // keydata + PYXMLSEC_DECLARE_NAMESPACE(keyDataCls, "KeyData"); + + PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataName, "NAME") + PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataValue, "VALUE") + PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataRetrievalMethod, "RETRIEVALMETHOD") + PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataEncryptedKey, "ENCRYPTEDKEY") + PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataAes, "AES") +#ifndef XMLSEC_NO_DES + PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataDes, "DES") +#endif +#ifndef XMLSEC_NO_DSA + PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataDsa, "DSA") +#endif +#if XMLSEC_VERSION_HEX > 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") + PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataX509, "X509") + PYXMLSEC_ADD_KEYDATA_CONSTANT(KeyDataRawX509Cert, "RAWX509CERT") + + PYXMLSEC_CLOSE_NAMESPACE(keyDataCls); +#undef PYXMLSEC_ADD_KEYDATA_CONSTANT + + +#define PYXMLSEC_ADD_TRANSFORM_CONSTANT(name, lname) \ + tmp = PyXmlSec_TransformNew(xmlSec ## name ## Id); \ + PYXMLSEC_ADD_CONSTANT(transformCls, name, lname); + + // transforms + PYXMLSEC_DECLARE_NAMESPACE(transformCls, "Transform"); + + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformInclC14N, "C14N"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformInclC14NWithComments, "C14N_COMMENTS"); + + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformInclC14N11, "C14N11"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformInclC14N11WithComments, "C14N11_COMMENTS"); + + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformExclC14N, "EXCL_C14N"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformExclC14NWithComments, "EXCL_C14N_COMMENTS"); + + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformEnveloped, "ENVELOPED"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformXPath, "XPATH"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformXPath2, "XPATH2"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformXPointer, "XPOINTER"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRemoveXmlTagsC14N, "REMOVE_XML_TAGS_C14N"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformVisa3DHack, "VISA3D_HACK"); + + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformAes128Cbc, "AES128"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformAes192Cbc, "AES192"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformAes256Cbc, "AES256"); + + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformKWAes128, "KW_AES128"); + 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 > 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"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformEcdsaSha384, "ECDSA_SHA384"); + 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"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRsaSha384, "RSA_SHA384"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformRsaSha512, "RSA_SHA512"); + 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"); + PYXMLSEC_ADD_TRANSFORM_CONSTANT(TransformSha256, "SHA256"); + 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 +#undef PYXMLSEC_ADD_CONSTANT +#undef PYXMLSEC_DECLARE_NAMESPACE + + if (PyModule_AddObject(package, "constants", constants) < 0) goto ON_FAIL; + + return 0; +ON_FAIL: + Py_XDECREF(tmp); + Py_XDECREF(nsCls); + Py_XDECREF(nodeCls); + Py_XDECREF(transformCls); + Py_XDECREF(encryptionTypeCls); + Py_XDECREF(keyFormatCls); + Py_XDECREF(keyDataCls); + Py_XDECREF(keyDataTypeCls); + Py_DECREF(constants); + + return -1; +} diff --git a/src/constants.h b/src/constants.h new file mode 100644 index 00000000..a9a5e716 --- /dev/null +++ b/src/constants.h @@ -0,0 +1,31 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __PYXMLSEC_CONSTANTS_H__ +#define __PYXMLSEC_CONSTANTS_H__ + +#include "platform.h" + +#include +#include + +typedef struct { + PyObject_HEAD + xmlSecTransformId id; +} PyXmlSec_Transform; + +typedef struct { + PyObject_HEAD + xmlSecKeyDataId id; +} PyXmlSec_KeyData; + +extern PyTypeObject* PyXmlSec_TransformType; +extern PyTypeObject* PyXmlSec_KeyDataType; + +#endif //__PYXMLSEC_CONSTANTS_H__ diff --git a/src/debug.h b/src/debug.h new file mode 100644 index 00000000..3f851bdc --- /dev/null +++ b/src/debug.h @@ -0,0 +1,25 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __PYXMLSEC_DEBUG_H__ +#define __PYXMLSEC_DEBUG_H__ + +#ifdef PYXMLSEC_ENABLE_DEBUG + +#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 new file mode 100644 index 00000000..d0b4bdf9 --- /dev/null +++ b/src/ds.c @@ -0,0 +1,637 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "common.h" +#include "platform.h" +#include "exception.h" +#include "constants.h" +#include "keys.h" +#include "lxml.h" + +#include + +typedef struct { + PyObject_HEAD + xmlSecDSigCtxPtr handle; + PyXmlSec_KeysManager* manager; +} PyXmlSec_SignatureContext; + +static PyObject* PyXmlSec_SignatureContext__new__(PyTypeObject *type, PyObject *args, PyObject *kwargs) { + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)PyType_GenericNew(type, args, kwargs); + PYXMLSEC_DEBUGF("%p: new sign context", ctx); + if (ctx != NULL) { + ctx->handle = NULL; + ctx->manager = NULL; + } + return (PyObject*)(ctx); +} + +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); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O&:__init__", kwlist, PyXmlSec_KeysManagerConvert, &manager)) { + goto ON_FAIL; + } + ctx->handle = xmlSecDSigCtxCreate(manager != NULL ? manager->handle : NULL); + if (ctx->handle == NULL) { + PyXmlSec_SetLastError("failed to create the digital signature context"); + goto ON_FAIL; + } + 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_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; + PYXMLSEC_DEBUGF("%p: delete sign context", self); + if (ctx->handle != NULL) { + xmlSecDSigCtxDestroy(ctx->handle); + } + // release manager object + Py_XDECREF(ctx->manager); + Py_TYPE(self)->tp_free(self); +} + +static const char PyXmlSec_SignatureContextKey__doc__[] = "Signature key.\n"; +static PyObject* PyXmlSec_SignatureContextKeyGet(PyObject* self, void* closure) { + 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; + } + key = (PyXmlSec_Key*)value; + + if (key->handle == NULL) { + PyErr_SetString(PyExc_TypeError, "empty key."); + return -1; + } + + if (ctx->handle->signKey != NULL) { + xmlSecKeyDestroy(ctx->handle->signKey); + } + + ctx->handle->signKey = xmlSecKeyDuplicate(key->handle); + if (ctx->handle->signKey == NULL) { + PyXmlSec_SetLastError("failed to duplicate key"); + return -1; + } + return 0; +} + +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}; + + PyXmlSec_LxmlElementPtr node = NULL; + const char* id_attr = "ID"; + const char* id_ns = NULL; + + xmlChar* name = NULL; + xmlAttrPtr attr; + xmlAttrPtr tmpAttr; + + PYXMLSEC_DEBUGF("%p: register id - start", self); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&|sz:register_id", kwlist, + PyXmlSec_LxmlElementConverter, &node, &id_attr, &id_ns)) + { + goto ON_FAIL; + } + + if (id_ns != NULL) { + attr = xmlHasNsProp(node->_c_node, XSTR(id_attr), XSTR(id_ns)); + } else { + attr = xmlHasProp(node->_c_node, XSTR(id_attr)); + } + + if (attr == NULL || attr->children == NULL) { + PyErr_SetString(PyXmlSec_Error, "missing attribute."); + goto ON_FAIL; + } + + name = xmlNodeListGetString(node->_c_node->doc, attr->children, 1); + tmpAttr = xmlGetID(node->_c_node->doc, name); + if (tmpAttr != attr) { + if (tmpAttr != NULL) { + PyErr_SetString(PyXmlSec_Error, "duplicated id."); + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + xmlAddID(NULL, node->_c_node->doc, name, attr); + Py_END_ALLOW_THREADS; + } + + xmlFree(name); + PYXMLSEC_DEBUGF("%p: register id - ok", self); + Py_RETURN_NONE; +ON_FAIL: + xmlFree(name); + PYXMLSEC_DEBUGF("%p: register id - fail", self); + return NULL; +} + +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; + } + + Py_BEGIN_ALLOW_THREADS; + rv = xmlSecDSigCtxSign(ctx->handle, node->_c_node); + PYXMLSEC_DUMP(xmlSecDSigCtxDebugDump, ctx->handle); + Py_END_ALLOW_THREADS; + if (rv < 0) { + PyXmlSec_SetLastError("failed to sign"); + goto ON_FAIL; + } + PYXMLSEC_DEBUGF("%p: sign - ok", self); + Py_RETURN_NONE; + +ON_FAIL: + PYXMLSEC_DEBUGF("%p: sign - fail", self); + return NULL; +} + +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; + } + + Py_BEGIN_ALLOW_THREADS; + 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 (ctx->handle->status != xmlSecDSigStatusSucceeded) { + PyErr_SetString(PyXmlSec_VerificationError, "Signature is invalid."); + goto ON_FAIL; + } + PYXMLSEC_DEBUGF("%p: verify - ok", self); + Py_RETURN_NONE; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: verify - fail", self); + return NULL; +} + +// common helper for operations binary_verify and binary_sign +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->handle->signKey == NULL) { + PyErr_SetString(PyXmlSec_Error, "Sign key is not specified."); + return -1; + } + + 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->handle->signMethod = xmlSecTransformCtxCreateAndAppend(&(ctx->handle->transformCtx), method); + if (ctx->handle->signMethod == NULL) { + PyXmlSec_SetLastError("could not create signature transform."); + return -1; + } + + 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->handle->signMethod, ctx->handle->signKey); + if (rv < 0) { + PyXmlSec_SetLastError("cannot set key."); + return -1; + } + ctx->handle->transformCtx.result = NULL; + ctx->handle->transformCtx.status = xmlSecTransformStatusNone; + + Py_BEGIN_ALLOW_THREADS; + rv = xmlSecTransformCtxBinaryExecute(&(ctx->handle->transformCtx), data, data_size); + Py_END_ALLOW_THREADS; + + if (rv < 0) { + PyXmlSec_SetLastError("failed to transform."); + return -1; + } + ctx->handle->result = ctx->handle->transformCtx.result; + + return 0; +} + +static const char PyXmlSec_SignatureContextSignBinary__doc__[] = \ + "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; + + PYXMLSEC_DEBUGF("%p: sign_binary - start", self); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s#O!:sign_binary", kwlist, + &data, &data_size, PyXmlSec_TransformType, &transform)) + { + goto ON_FAIL; + } + + ctx->handle->operation = xmlSecTransformOperationSign; + + if (PyXmlSec_ProcessSignBinary(ctx, (const xmlSecByte*)data, (xmlSecSize)data_size, transform->id) != 0) { + goto ON_FAIL; + } + + PYXMLSEC_DEBUGF("%p: sign_binary - ok", self); + return PyBytes_FromStringAndSize( + (const char*)xmlSecBufferGetData(ctx->handle->result), + (Py_ssize_t)xmlSecBufferGetSize(ctx->handle->result) + ); +ON_FAIL: + PYXMLSEC_DEBUGF("%p: sign_binary - fail", self); + return NULL; +} + +static const char PyXmlSec_SignatureContextVerifyBinary__doc__[] = \ + "verify_binary(bytes, transform, signature) -> None\n" + "Verifies signature for binary data.\n\n" + ":param bytes: the binary data\n" + ":type bytes: :class:`bytes`\n" + ":param transform: the signature algorithm\n" + ":type transform: :class:`__Transform`\n" + ":param signature: the signature\n" + ":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, + &data, &data_size, PyXmlSec_TransformType, &transform, &sign, &sign_size)) + { + goto ON_FAIL; + } + + ctx->handle->operation = xmlSecTransformOperationVerify; + if (PyXmlSec_ProcessSignBinary(ctx, (const xmlSecByte*)data, (xmlSecSize)data_size, transform->id) != 0) { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + rv = xmlSecTransformVerify(ctx->handle->signMethod, (const xmlSecByte*)sign, (xmlSecSize)sign_size, &(ctx->handle->transformCtx)); + Py_END_ALLOW_THREADS; + + if (rv < 0) { + PyXmlSec_SetLastError2(PyXmlSec_VerificationError, "Cannot verify signature."); + goto ON_FAIL; + } + + if (ctx->handle->signMethod->status != xmlSecTransformStatusOk) { + PyXmlSec_SetLastError2(PyXmlSec_VerificationError, "Signature is invalid."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUGF("%p: verify binary - ok", self); + Py_RETURN_NONE; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: verify binary - fail", self); + return NULL; +} + +static const char PyXmlSec_SignatureContextEnableReferenceTransform__doc__[] = \ + "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)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + rv = xmlSecDSigCtxEnableReferenceTransform(ctx->handle, transform->id); + Py_END_ALLOW_THREADS; + + if (rv < 0) { + PyXmlSec_SetLastError("cannot enable reference transform."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUGF("%p: enable_reference_transform - ok", self); + Py_RETURN_NONE; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: enable_reference_transform - fail", self); + return NULL; +} + +static const char PyXmlSec_SignatureContextEnableSignatureTransform__doc__[] = \ + "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; + } + + Py_BEGIN_ALLOW_THREADS; + rv = xmlSecDSigCtxEnableSignatureTransform(ctx->handle, transform->id); + Py_END_ALLOW_THREADS; + + if (rv < 0) { + PyXmlSec_SetLastError("cannot enable signature transform."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUGF("%p: enable_signature_transform - ok", self); + Py_RETURN_NONE; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: enable_signature_transform - fail", self); + return NULL; +} + +static const char PyXmlSec_SignatureContextSetEnabledKeyData__doc__[] = \ + "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)) { + goto ON_FAIL; + } + if ((iter = PyObject_GetIter(keydata_list)) == NULL) goto ON_FAIL; + + enabled_list = &(ctx->handle->keyInfoReadCtx.enabledKeyData); + xmlSecPtrListEmpty(enabled_list); + + while ((item = PyIter_Next(iter)) != NULL) { + if (!PyObject_IsInstance(item, (PyObject*)PyXmlSec_KeyDataType)) { + PyErr_SetString(PyExc_TypeError, "expected list of KeyData constants."); + goto ON_FAIL; + } + if (xmlSecPtrListAdd(enabled_list, (xmlSecPtr)((PyXmlSec_KeyData*)item)->id) < 0) { + PyXmlSec_SetLastError("cannot set enabled key."); + goto ON_FAIL; + } + Py_DECREF(item); + } + Py_DECREF(iter); + + PYXMLSEC_DEBUGF("%p: set_enabled_key_data - ok", self); + + Py_RETURN_NONE; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: set_enabled_key_data - fail", self); + Py_XDECREF(item); + Py_XDECREF(iter); + return NULL; +} + +static PyGetSetDef PyXmlSec_SignatureContextGetSet[] = { + { + "key", + (getter)PyXmlSec_SignatureContextKeyGet, + (setter)PyXmlSec_SignatureContextKeySet, + (char*)PyXmlSec_SignatureContextKey__doc__, + NULL + }, + {NULL} /* Sentinel */ +}; + +static PyMethodDef PyXmlSec_SignatureContextMethods[] = { + { + "register_id", + (PyCFunction)PyXmlSec_SignatureContextRegisterId, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_SignatureContextRegisterId__doc__, + }, + { + "sign", + (PyCFunction)PyXmlSec_SignatureContextSign, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_SignatureContextSign__doc__ + }, + { + "verify", + (PyCFunction)PyXmlSec_SignatureContextVerify, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_SignatureContextVerify__doc__ + }, + { + "sign_binary", + (PyCFunction)PyXmlSec_SignatureContextSignBinary, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_SignatureContextSignBinary__doc__ + }, + { + "verify_binary", + (PyCFunction)PyXmlSec_SignatureContextVerifyBinary, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_SignatureContextVerifyBinary__doc__ + }, + { + "enable_reference_transform", + (PyCFunction)PyXmlSec_SignatureContextEnableReferenceTransform, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_SignatureContextEnableReferenceTransform__doc__ + }, + { + "enable_signature_transform", + (PyCFunction)PyXmlSec_SignatureContextEnableSignatureTransform, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_SignatureContextEnableSignatureTransform__doc__, + }, + { + "set_enabled_key_data", + (PyCFunction)PyXmlSec_SignatureContextSetEnabledKeyData, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_SignatureContextSetEnabledKeyData__doc__, + }, + {NULL, NULL} /* sentinel */ +}; + +static PyTypeObject _PyXmlSec_SignatureContextType = { + PyVarObject_HEAD_INIT(NULL, 0) + STRINGIFY(MODULE_NAME) ".SignatureContext", /* tp_name */ + sizeof(PyXmlSec_SignatureContext), /* tp_basicsize */ + 0, /* tp_itemsize */ + PyXmlSec_SignatureContext__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|Py_TPFLAGS_BASETYPE, /* tp_flags */ + "XML Digital Signature implementation", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + PyXmlSec_SignatureContextMethods, /* tp_methods */ + 0, /* tp_members */ + PyXmlSec_SignatureContextGetSet, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + PyXmlSec_SignatureContext__init__, /* tp_init */ + 0, /* tp_alloc */ + PyXmlSec_SignatureContext__new__, /* tp_new */ + 0, /* tp_free */ +}; + +PyTypeObject* PyXmlSec_SignatureContextType = &_PyXmlSec_SignatureContextType; + +int PyXmlSec_DSModule_Init(PyObject* package) { + if (PyType_Ready(PyXmlSec_SignatureContextType) < 0) goto ON_FAIL; + + // since objects is created as static objects, need to increase refcount to prevent deallocate + Py_INCREF(PyXmlSec_SignatureContextType); + + if (PyModule_AddObject(package, "SignatureContext", (PyObject*)PyXmlSec_SignatureContextType) < 0) goto ON_FAIL; + return 0; +ON_FAIL: + return -1; +} diff --git a/src/enc.c b/src/enc.c new file mode 100644 index 00000000..42195dd3 --- /dev/null +++ b/src/enc.c @@ -0,0 +1,552 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "common.h" +#include "platform.h" +#include "exception.h" +#include "constants.h" +#include "keys.h" +#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 + xmlSecEncCtxPtr handle; + PyXmlSec_KeysManager* manager; +} PyXmlSec_EncryptionContext; + +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 enc context", ctx); + if (ctx != NULL) { + ctx->handle = NULL; + ctx->manager = NULL; + } + return (PyObject*)(ctx); +} + +static int PyXmlSec_EncryptionContext__init__(PyObject* self, PyObject* args, PyObject* kwargs) { + static char *kwlist[] = { "manager", NULL}; + + PyXmlSec_KeysManager* manager = NULL; + PyXmlSec_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; + + PYXMLSEC_DEBUGF("%p: init enc context", self); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O&:__init__", kwlist, PyXmlSec_KeysManagerConvert, &manager)) { + goto ON_FAIL; + } + ctx->handle = xmlSecEncCtxCreate(manager != NULL ? manager->handle : NULL); + if (ctx->handle == NULL) { + PyXmlSec_SetLastError("failed to create the encryption context"); + goto ON_FAIL; + } + 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_EncryptionContext* ctx = (PyXmlSec_EncryptionContext*)self; + + PYXMLSEC_DEBUGF("%p: delete enc context", self); + + if (ctx->handle != NULL) { + xmlSecEncCtxDestroy(ctx->handle); + } + // release manager object + Py_XDECREF(ctx->manager); + Py_TYPE(self)->tp_free(self); +} + +static const char PyXmlSec_EncryptionContextKey__doc__[] = "Encryption key.\n"; +static PyObject* PyXmlSec_EncryptionContextKeyGet(PyObject* self, void* closure) { + 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; + } + + key = (PyXmlSec_Key*)value; + if (key->handle == NULL) { + PyErr_SetString(PyExc_TypeError, "empty key."); + return -1; + } + + if (ctx->handle->encKey != NULL) { + xmlSecKeyDestroy(ctx->handle->encKey); + } + + ctx->handle->encKey = xmlSecKeyDuplicate(key->handle); + if (ctx->handle->encKey == NULL) { + PyXmlSec_SetLastError("failed to duplicate key"); + return -1; + } + 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__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &template, &data, &data_size)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + 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) { + PyXmlSec_SetLastError("failed to encrypt binary"); + goto ON_FAIL; + } + Py_INCREF(template); + PYXMLSEC_DEBUGF("%p: encrypt_binary - ok", self); + + return (PyObject*)template; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: encrypt_binary - fail", self); + return NULL; +} + +// 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 + 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__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &template, PyXmlSec_LxmlElementConverter, &node)) + { + goto ON_FAIL; + } + 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`"); + goto ON_FAIL; + } + + // `xmlSecEncCtxXmlEncrypt` will replace the subtree rooted + // 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 + 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 + Py_BEGIN_ALLOW_THREADS; + if (template->_doc->_c_doc != node->_doc->_c_doc) { + // `xmlSecEncCtxEncrypt` expects *template* to belong to the document of *node* + // if this is not the case, we copy the `libxml2` subtree there. + xnew_node = xmlDocCopyNode(template->_c_node, node->_doc->_c_doc, 1); // recursive + if (xnew_node == NULL) { + rv = 1; + } + } + 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->handle, node->_doc); + if (NULL != PyErr_Occurred()) { + goto ON_FAIL; + } + + if (rv != 0) { + if (rv > 0) { + PyErr_SetString(PyXmlSec_InternalError, "could not copy template tree"); + } else { + PyXmlSec_SetLastError("failed to encrypt xml"); + } + goto ON_FAIL; + } + + 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: + PYXMLSEC_DEBUGF("%p: encrypt_xml - fail", self); + xmlFree(tmpType); + return NULL; +} + +static const char PyXmlSec_EncryptionContextEncryptUri__doc__[] = \ + "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; + } + + Py_BEGIN_ALLOW_THREADS; + rv = xmlSecEncCtxUriEncrypt(ctx->handle, template->_c_node, (const xmlSecByte*)uri); + PYXMLSEC_DUMP(xmlSecEncCtxDebugDump, ctx->handle); + Py_END_ALLOW_THREADS; + + if (rv < 0) { + PyXmlSec_SetLastError("failed to encrypt URI"); + goto ON_FAIL; + } + PYXMLSEC_DEBUGF("%p: encrypt_uri - ok", self); + Py_INCREF(template); + return (PyObject*)template; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: encrypt_uri - fail", self); + return NULL; +} + +static const char PyXmlSec_EncryptionContextDecrypt__doc__[] = \ + "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; + } + + xparent = node->_c_node->parent; + if (xparent != NULL && !PyXmlSec_IsElement(xparent)) { + xparent = NULL; + } + + if (xparent != NULL) { + parent = (PyObject*)PyXmlSec_elementFactory(node->_doc, xparent); + if (parent == NULL) { + PyErr_SetString(PyXmlSec_InternalError, "failed to construct parent"); + goto ON_FAIL; + } + // get index of node + node_num = PyObject_CallMethod(parent, "index", "O", node); + PYXMLSEC_DEBUGF("parent: %p, %p", parent, node_num); + } + + Py_BEGIN_ALLOW_THREADS; + 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->handle, node->_doc); + + if (rv < 0) { + PyXmlSec_SetLastError("failed to decrypt"); + goto ON_FAIL; + } + + if (!ctx->handle->resultReplaced) { + Py_XDECREF(node_num); + Py_XDECREF(parent); + PYXMLSEC_DEBUGF("%p: binary.decrypt - ok", self); + return PyBytes_FromStringAndSize( + (const char*)xmlSecBufferGetData(ctx->handle->result), + (Py_ssize_t)xmlSecBufferGetSize(ctx->handle->result) + ); + } + + if (xparent != NULL) { + ttype = xmlGetProp(node->_c_node, XSTR("Type")); + notContent = (ttype == NULL || !xmlStrEqual(ttype, xmlSecTypeEncContent)); + xmlFree(ttype); + + if (notContent) { + tmp = PyObject_GetItem(parent, node_num); + if (tmp == NULL) goto ON_FAIL; + Py_DECREF(parent); + parent = tmp; + } + Py_DECREF(node_num); + PYXMLSEC_DEBUGF("%p: parent.decrypt - ok", self); + return parent; + } + + // root has been replaced + root = xmlDocGetRootElement(node->_doc->_c_doc); + if (root == NULL) { + PyErr_SetString(PyXmlSec_Error, "decryption resulted in a non well formed document"); + goto ON_FAIL; + } + + Py_XDECREF(node_num); + Py_XDECREF(parent); + + PYXMLSEC_DEBUGF("%p: decrypt - ok", self); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, root); + +ON_FAIL: + PYXMLSEC_DEBUGF("%p: decrypt - fail", self); + Py_XDECREF(node_num); + Py_XDECREF(parent); + return NULL; +} + +static PyGetSetDef PyXmlSec_EncryptionContextGetSet[] = { + { + "key", + (getter)PyXmlSec_EncryptionContextKeyGet, + (setter)PyXmlSec_EncryptionContextKeySet, + (char*)PyXmlSec_EncryptionContextKey__doc__, + NULL + }, + {NULL} /* Sentinel */ +}; + +static PyMethodDef PyXmlSec_EncryptionContextMethods[] = { + { + "reset", + (PyCFunction)PyXmlSec_EncryptionContextReset, + METH_NOARGS, + PyXmlSec_EncryptionContextReset__doc__, + }, + { + "encrypt_binary", + (PyCFunction)PyXmlSec_EncryptionContextEncryptBinary, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_EncryptionContextEncryptBinary__doc__, + }, + { + "encrypt_xml", + (PyCFunction)PyXmlSec_EncryptionContextEncryptXml, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_EncryptionContextEncryptXml__doc__ + }, + { + "encrypt_uri", + (PyCFunction)PyXmlSec_EncryptionContextEncryptUri, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_EncryptionContextEncryptUri__doc__ + }, + { + "decrypt", + (PyCFunction)PyXmlSec_EncryptionContextDecrypt, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_EncryptionContextDecrypt__doc__ + }, + {NULL, NULL} /* sentinel */ +}; + +static PyTypeObject _PyXmlSec_EncryptionContextType = { + PyVarObject_HEAD_INIT(NULL, 0) + STRINGIFY(MODULE_NAME) ".EncryptionContext", /* tp_name */ + sizeof(PyXmlSec_EncryptionContext), /* tp_basicsize */ + 0, /* tp_itemsize */ + PyXmlSec_EncryptionContext__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|Py_TPFLAGS_BASETYPE, /* tp_flags */ + "XML Encryption implementation", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + PyXmlSec_EncryptionContextMethods, /* tp_methods */ + 0, /* tp_members */ + PyXmlSec_EncryptionContextGetSet, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + PyXmlSec_EncryptionContext__init__, /* tp_init */ + 0, /* tp_alloc */ + PyXmlSec_EncryptionContext__new__, /* tp_new */ + 0 /* tp_free */ +}; + +PyTypeObject* PyXmlSec_EncryptionContextType = &_PyXmlSec_EncryptionContextType; + +int PyXmlSec_EncModule_Init(PyObject* package) { + if (PyType_Ready(PyXmlSec_EncryptionContextType) < 0) goto ON_FAIL; + + PYXMLSEC_DEBUGF("%p", PyXmlSec_EncryptionContextType); + // since objects is created as static objects, need to increase refcount to prevent deallocate + Py_INCREF(PyXmlSec_EncryptionContextType); + + if (PyModule_AddObject(package, "EncryptionContext", (PyObject*)PyXmlSec_EncryptionContextType) < 0) goto ON_FAIL; + return 0; +ON_FAIL: + return -1; +} diff --git a/src/exception.c b/src/exception.c new file mode 100644 index 00000000..ac0e44ee --- /dev/null +++ b/src/exception.c @@ -0,0 +1,227 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "common.h" +#include "exception.h" +#include "utils.h" + +#include +#include + +#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; + const xmlChar* func; + const xmlChar* object; + const xmlChar* subject; + const xmlChar* msg; + int line; + int reason; +} PyXmlSec_ErrorHolder; + +PyXmlSec_ErrorHolder* PyXmlSec_ErrorHolderCreate(const char* file, int line, const char* func, const char* object, const char* subject, int reason, const char* msg) { + PyXmlSec_ErrorHolder* h = (PyXmlSec_ErrorHolder*)xmlMalloc(sizeof(PyXmlSec_ErrorHolder)); + + // file and func is __FILE__ and __FUNCTION__ macro, so it can be stored as is. + h->file = XSTR(file); + h->line = line; + h->func = XSTR(func); + h->reason = reason; + // there is no guarantee that object and subject will not be deallocate after exit from function, + // so make a copy + // xmlCharStrdup returns NULL if arg is NULL + h->object = xmlCharStrdup(object); + h->subject = xmlCharStrdup(subject); + h->msg = xmlCharStrdup(msg); + + PYXMLSEC_DEBUGF("new error %p", h); + return h; +} + +void PyXmlSec_ErrorHolderFree(PyXmlSec_ErrorHolder* h) { + if (h != NULL) { + PYXMLSEC_DEBUGF("free error %p", h); + xmlFree((void*)(h->object)); + xmlFree((void*)(h->subject)); + xmlFree((void*)(h->msg)); + xmlFree((void*)(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 + #if PY_MINOR_VERSION >= 7 + v = (PyXmlSec_ErrorHolder*)PyThread_tss_get(&PyXmlSec_LastErrorKey); + //PyThread_tss_delete(&PyXmlSec_LastErrorKey); + r = PyThread_tss_set(&PyXmlSec_LastErrorKey, (void*)e); + #else + v = (PyXmlSec_ErrorHolder*)PyThread_get_key_value(PyXmlSec_LastErrorKey); + PyThread_delete_key_value(PyXmlSec_LastErrorKey); + r = PyThread_set_key_value(PyXmlSec_LastErrorKey, (void*)e); + #endif + PYXMLSEC_DEBUGF("set_key_value returns %d", r); + return v; +} + +// xmlsec library error callback +static void PyXmlSec_ErrorCallback(const char* file, int line, const char* func, const char* object, const char* subject, int reason, const char* msg) { + // TODO do not allocate error object each time. + PyXmlSec_ErrorHolderFree(PyXmlSec_ExchangeLastError(PyXmlSec_ErrorHolderCreate(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; + } + + exc = PyObject_CallFunction(type, "is", h->reason, msg); + if (exc == NULL) goto ON_FAIL; + + PyXmlSec_SetLongAttr(exc, "code", h->reason); + PyXmlSec_SetStringAttr(exc, "message", msg); + PyXmlSec_SetStringAttr(exc, "details", (const char*)xmlSecErrorsSafeString(h->msg)); + PyXmlSec_SetStringAttr(exc, "file", (const char*)xmlSecErrorsSafeString(h->file)); + PyXmlSec_SetLongAttr(exc, "line", h->line); + PyXmlSec_SetStringAttr(exc, "func", (const char*)xmlSecErrorsSafeString(h->func)); + PyXmlSec_SetStringAttr(exc, "object", (const char*)xmlSecErrorsSafeString(h->object)); + PyXmlSec_SetStringAttr(exc, "subject", (const char*)xmlSecErrorsSafeString(h->subject)); + +ON_FAIL: + PyXmlSec_ErrorHolderFree(h); + return exc; +} + +void PyXmlSec_SetLastError2(PyObject* type, const char* msg) { + PyObject* last = PyXmlSec_GetLastError(type, msg); + if (last == NULL) { + PYXMLSEC_DEBUG("WARNING: no xmlsec error"); + last = PyObject_CallFunction(PyXmlSec_InternalError, "is", (int)-1, msg); + if (last == NULL) { + return; + } + } + PyErr_SetObject(type, last); + Py_DECREF(last); +} + +void PyXmlSec_SetLastError(const char* msg) { + PyXmlSec_SetLastError2(PyXmlSec_Error, msg); +} + +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; + PyXmlSec_InternalError = NULL; + PyXmlSec_VerificationError = NULL; + + if ((PyXmlSec_Error = PyErr_NewExceptionWithDoc( + STRINGIFY(MODULE_NAME) ".Error", "The common exception class.", PyExc_Exception, 0)) == NULL) goto ON_FAIL; + + if ((PyXmlSec_InternalError = PyErr_NewExceptionWithDoc( + STRINGIFY(MODULE_NAME) ".InternalError", "The internal exception class.", PyXmlSec_Error, 0)) == NULL) goto ON_FAIL; + + if ((PyXmlSec_VerificationError = PyErr_NewExceptionWithDoc( + STRINGIFY(MODULE_NAME) ".VerificationError", "The verification exception class.", PyXmlSec_Error, 0)) == NULL) goto ON_FAIL; + + if (PyModule_AddObject(package, "Error", PyXmlSec_Error) < 0) goto ON_FAIL; + if (PyModule_AddObject(package, "InternalError", PyXmlSec_InternalError) < 0) goto ON_FAIL; + if (PyModule_AddObject(package, "VerificationError", PyXmlSec_VerificationError) < 0) goto ON_FAIL; + + #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; + +ON_FAIL: + Py_XDECREF(PyXmlSec_Error); + Py_XDECREF(PyXmlSec_InternalError); + Py_XDECREF(PyXmlSec_VerificationError); + return -1; +} diff --git a/src/exception.h b/src/exception.h new file mode 100644 index 00000000..687cd778 --- /dev/null +++ b/src/exception.h @@ -0,0 +1,29 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __PYXMLSEC_EXCEPTIONS_H__ +#define __PYXMLSEC_EXCEPTIONS_H__ + +#include "platform.h" + +extern PyObject* PyXmlSec_Error; +extern PyObject* PyXmlSec_InternalError; +extern PyObject* PyXmlSec_VerificationError; + +void PyXmlSec_SetLastError(const char* msg); + +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 new file mode 100644 index 00000000..5ff04aae --- /dev/null +++ b/src/keys.c @@ -0,0 +1,913 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "common.h" +#include "constants.h" +#include "exception.h" +#include "keys.h" +#include "utils.h" + +#include + + +static PyObject* PyXmlSec_Key__new__(PyTypeObject *type, PyObject *args, PyObject *kwargs) { + PyXmlSec_Key* key = (PyXmlSec_Key*)PyType_GenericNew(type, args, kwargs); + PYXMLSEC_DEBUGF("%p: new key", key); + if (key != NULL) { + key->handle = NULL; + key->is_own = 0; + } + return (PyObject*)(key); +} + +static void PyXmlSec_Key__del__(PyObject* 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); + } + Py_TYPE(self)->tp_free(self); +} + +static PyXmlSec_Key* PyXmlSec_NewKey1(PyTypeObject* type) { + return (PyXmlSec_Key*)PyObject_CallFunctionObjArgs((PyObject*)type, NULL); +} + +static PyObject* PyXmlSec_Key__copy__(PyObject* self) { + xmlSecKeyPtr handle = ((PyXmlSec_Key*)self)->handle; + PyXmlSec_Key* key2; + + PYXMLSEC_DEBUGF("%p: copy key", self); + + key2 = PyXmlSec_NewKey1(Py_TYPE(self)); + + if (handle == NULL || key2 == NULL) { + PYXMLSEC_DEBUGF("%p: null key", self); + return (PyObject*)key2; + } + + Py_BEGIN_ALLOW_THREADS; + key2->handle = xmlSecKeyDuplicate(handle); + Py_END_ALLOW_THREADS; + + if (key2->handle == NULL) { + PYXMLSEC_DEBUGF("%p: failed to duplicate key", self); + PyXmlSec_SetLastError("cannot duplicate key"); + Py_DECREF(key2); + return NULL; + } + key2->is_own = 1; + return (PyObject*)key2; +} + +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}; + + const char* data = NULL; + Py_ssize_t data_size = 0; + const char* password = NULL; + unsigned int format = 0; + + PyXmlSec_Key* key = NULL; + + PYXMLSEC_DEBUG("load key from memory - start"); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s#I|z:from_memory", kwlist, &data, &data_size, &format, &password)) { + goto ON_FAIL; + } + + if ((key = PyXmlSec_NewKey1((PyTypeObject*)self)) == NULL) goto ON_FAIL; + + Py_BEGIN_ALLOW_THREADS; + key->handle = xmlSecCryptoAppKeyLoadMemory((const xmlSecByte*)data, (xmlSecSize)data_size, format, password, NULL, NULL); + Py_END_ALLOW_THREADS; + + if (key->handle == NULL) { + PyXmlSec_SetLastError("cannot load key"); + goto ON_FAIL; + } + + key->is_own = 1; + + PYXMLSEC_DEBUG("load key from memory - ok"); + + return (PyObject*)key; + +ON_FAIL: + PYXMLSEC_DEBUG("load key from memory - fail"); + Py_XDECREF(key); + return NULL; +} + +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[] = { "file", "format", "password", NULL}; + + PyObject* file = NULL; + const char* password = NULL; + unsigned int format = 0; + + PyXmlSec_Key* key = NULL; + PyObject* bytes = NULL; + int is_content = 0; + const char* data = NULL; + Py_ssize_t data_size = 0; + + PYXMLSEC_DEBUG("load key from file - start"); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OI|z:from_file", kwlist, &file, &format, &password)) { + goto ON_FAIL; + } + + bytes = PyXmlSec_GetFilePathOrContent(file, &is_content); + if (bytes == NULL) goto ON_FAIL; + + if (is_content == 1) { + data = PyBytes_AsStringAndSize2(bytes, &data_size); + } else { + data = PyBytes_AsString(bytes); + } + + if (data == NULL) goto ON_FAIL; + + if ((key = PyXmlSec_NewKey1((PyTypeObject*)self)) == NULL) goto ON_FAIL; + + Py_BEGIN_ALLOW_THREADS; + if (is_content) { + key->handle = xmlSecCryptoAppKeyLoadMemory((const xmlSecByte*)data, (xmlSecSize)data_size, format, password, NULL, NULL); + } else { + #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; + + if (key->handle == NULL) { + PyXmlSec_SetLastError("cannot read key"); + goto ON_FAIL; + } + + key->is_own = 1; + Py_DECREF(bytes); + + PYXMLSEC_DEBUG("load key from file - ok"); + return (PyObject*)key; + +ON_FAIL: + PYXMLSEC_DEBUG("load key from file - fail"); + Py_XDECREF(key); + Py_XDECREF(bytes); + return NULL; +} + +static const char PyXmlSec_KeyFromEngine__doc__[] = \ + "from_engine(engine_and_key_id) -> xmlsec.Key\n" + "Loads PKI key from an engine.\n\n" + ":param engine_and_key_id: engine and key id, i.e. 'pkcs11;pkcs11:token=XmlsecToken;object=XmlsecKey;pin-value=password'\n" + ":type engine_and_key_id: :class:`str`, " + ":return: pointer to newly created key\n" + ":rtype: :class:`~xmlsec.Key`"; +static PyObject* PyXmlSec_KeyFromEngine(PyObject* self, PyObject* args, PyObject* kwargs) { + static char *kwlist[] = {"engine_and_key_id", NULL}; + + const char* engine_and_key_id = NULL; + PyXmlSec_Key* key = NULL; + + PYXMLSEC_DEBUG("load key from engine - start"); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s:from_engine", kwlist, &engine_and_key_id)) { + goto ON_FAIL; + } + + if ((key = PyXmlSec_NewKey1((PyTypeObject*)self)) == NULL) goto ON_FAIL; + + Py_BEGIN_ALLOW_THREADS; + #if XMLSEC_VERSION_HEX >= 0x10303 + // from version 1.3.3 (inclusive) + key->handle = xmlSecCryptoAppKeyLoadEx(engine_and_key_id, xmlSecKeyDataTypePrivate, xmlSecKeyDataFormatEngine, NULL, xmlSecCryptoAppGetDefaultPwdCallback(), (void*)engine_and_key_id); + #else + key->handle = xmlSecCryptoAppKeyLoad(engine_and_key_id, xmlSecKeyDataFormatEngine, NULL, xmlSecCryptoAppGetDefaultPwdCallback(), (void*)engine_and_key_id); + #endif + Py_END_ALLOW_THREADS; + + if (key->handle == NULL) { + PyXmlSec_SetLastError("cannot read key"); + goto ON_FAIL; + } + + key->is_own = 1; + + PYXMLSEC_DEBUG("load key from engine - ok"); + return (PyObject*)key; + +ON_FAIL: + PYXMLSEC_DEBUG("load key from engine - fail"); + Py_XDECREF(key); + return NULL; +} + +static const char PyXmlSec_KeyGenerate__doc__[] = \ + "generate(klass, size, type) -> xmlsec.Key\n" + "Generates key of kind ``klass`` with ``size`` and ``type``.\n\n" + ":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[] = { "klass", "size", "type", NULL}; + + PyXmlSec_KeyData* keydata = NULL; + short unsigned int keysize = 0; + unsigned int keytype = 0; + + PyXmlSec_Key* key = NULL; + + PYXMLSEC_DEBUG("generate new key - start"); + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!HI:generate", kwlist, PyXmlSec_KeyDataType, &keydata, &keysize, &keytype)) { + goto ON_FAIL; + } + if ((key = PyXmlSec_NewKey1((PyTypeObject*)self)) == NULL) goto ON_FAIL; + + Py_BEGIN_ALLOW_THREADS; + key->handle = xmlSecKeyGenerate(keydata->id, keysize, keytype); + Py_END_ALLOW_THREADS; + + if (key->handle == NULL) { + PyXmlSec_SetLastError("cannot generate key"); + goto ON_FAIL; + } + key->is_own = 1; + PYXMLSEC_DEBUG("generate new key - ok"); + return (PyObject*)key; + +ON_FAIL: + PYXMLSEC_DEBUG("generate new key - fail"); + Py_XDECREF(key); + return NULL; +} + +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[] = { "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, PyUnicode_FSConverter, &filepath)) + { + goto ON_FAIL; + } + + filename = PyBytes_AsString(filepath); + if (filename == NULL) goto ON_FAIL; + if ((key = PyXmlSec_NewKey1((PyTypeObject*)self)) == NULL) goto ON_FAIL; + + Py_BEGIN_ALLOW_THREADS; + key->handle = xmlSecKeyReadBinaryFile(keydata->id, filename); + Py_END_ALLOW_THREADS; + + if (key->handle == NULL) { + PyXmlSec_SetLastError("cannot read key"); + goto ON_FAIL; + } + + key->is_own = 1; + Py_DECREF(filepath); + + PYXMLSEC_DEBUG("load symmetric key - ok"); + return (PyObject*)key; + +ON_FAIL: + PYXMLSEC_DEBUG("load symmetric key - fail"); + Py_XDECREF(key); + Py_XDECREF(filepath); + return NULL; +} + +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; + } + + Py_BEGIN_ALLOW_THREADS; + rv = xmlSecCryptoAppKeyCertLoadMemory(key->handle, (const xmlSecByte*)data, (xmlSecSize)data_size, format); + Py_END_ALLOW_THREADS; + if (rv < 0) { + PyXmlSec_SetLastError("cannot load cert"); + goto ON_FAIL; + } + Py_XDECREF(tmp); + PYXMLSEC_DEBUGF("%p: load certificate from memory - ok", self); + Py_RETURN_NONE; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: load certificate from memory - fail", self); + Py_XDECREF(tmp); + return NULL; +} + +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[] = { "file", "format", NULL}; + + PyXmlSec_Key* key = (PyXmlSec_Key*)self; + + PyObject* file = NULL; + unsigned int format = 0; + + PyObject* bytes = NULL; + 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)) { + goto ON_FAIL; + } + bytes = PyXmlSec_GetFilePathOrContent(file, &is_content); + if (bytes == NULL) goto ON_FAIL; + + if (is_content == 1) { + data = PyBytes_AsStringAndSize2(bytes, &data_size); + } else { + data = PyBytes_AsString(bytes); + } + + if (data == NULL) goto ON_FAIL; + + Py_BEGIN_ALLOW_THREADS; + if (is_content) { + rv = xmlSecCryptoAppKeyCertLoadMemory(key->handle, (const xmlSecByte*)data, (xmlSecSize)data_size, format); + } else { + rv = xmlSecCryptoAppKeyCertLoad(key->handle, data, format); + } + Py_END_ALLOW_THREADS; + if (rv < 0) { + PyXmlSec_SetLastError("cannot load cert"); + goto ON_FAIL; + } + Py_DECREF(bytes); + + PYXMLSEC_DEBUGF("%p: load certificate from file - ok", self); + Py_RETURN_NONE; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: load certificate from file - fail", self); + Py_XDECREF(bytes); + return NULL; +} + +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); + if (key->handle == NULL) { + PyErr_SetString(PyExc_ValueError, "key is not ready"); + return NULL; + } + 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); + + if (key->handle == NULL) { + PyErr_SetString(PyExc_ValueError, "key is not ready"); + return -1; + } + + 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; + + if (xmlSecKeySetName(key->handle, XSTR(name)) < 0) { + PyXmlSec_SetLastError("cannot set name"); + return -1; + } + return 0; +} + +static PyGetSetDef PyXmlSec_KeyGetSet[] = { + { + "name", + (getter)PyXmlSec_KeyNameGet, + (setter)PyXmlSec_KeyNameSet, + (char*)PyXmlSec_KeyName__doc__, + NULL + }, + {NULL} /* Sentinel */ +}; + +static PyMethodDef PyXmlSec_KeyMethods[] = { + { + "from_memory", + (PyCFunction)PyXmlSec_KeyFromMemory, + METH_CLASS|METH_VARARGS|METH_KEYWORDS, + PyXmlSec_KeyFromMemory__doc__, + }, + { + "from_file", + (PyCFunction)PyXmlSec_KeyFromFile, + 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, + METH_CLASS|METH_VARARGS|METH_KEYWORDS, + PyXmlSec_KeyGenerate__doc__ + }, + { + "from_binary_file", + (PyCFunction)PyXmlSec_KeyFromBinaryFile, + 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, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_KeyCertFromMemory__doc__ + }, + { + "load_cert_from_file", + (PyCFunction)PyXmlSec_KeyCertFromFile, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_KeyCertFromFile__doc__ + }, + { + "__copy__", + (PyCFunction)PyXmlSec_Key__copy__, + METH_NOARGS, + "", + }, + { + "__deepcopy__", + (PyCFunction)PyXmlSec_Key__copy__, + METH_NOARGS, + "", + }, + {NULL, NULL} /* sentinel */ +}; + +static PyTypeObject _PyXmlSec_KeyType = { + PyVarObject_HEAD_INIT(NULL, 0) + STRINGIFY(MODULE_NAME) ".Key", /* tp_name */ + sizeof(PyXmlSec_Key), /* tp_basicsize */ + 0, /* tp_itemsize */ + PyXmlSec_Key__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|Py_TPFLAGS_BASETYPE, /* tp_flags */ + "Key", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + PyXmlSec_KeyMethods, /* tp_methods */ + 0, /* tp_members */ + PyXmlSec_KeyGetSet, /* 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 */ + PyXmlSec_Key__new__, /* tp_new */ + 0, /* tp_free */ +}; + +PyTypeObject* PyXmlSec_KeyType = &_PyXmlSec_KeyType; + +// creates a new key object +PyXmlSec_Key* PyXmlSec_NewKey(void) { + return PyXmlSec_NewKey1(PyXmlSec_KeyType); +} + +/// key manager class + +static PyObject* PyXmlSec_KeysManager__new__(PyTypeObject *type, PyObject *args, PyObject *kwargs) { + PyXmlSec_KeysManager* mgr = (PyXmlSec_KeysManager*)PyType_GenericNew(type, args, kwargs); + PYXMLSEC_DEBUGF("%p: new manager", mgr); + if (mgr != NULL) { + mgr->handle = NULL; + } + return (PyObject*)(mgr); +} + +static int PyXmlSec_KeysManager__init__(PyObject* self, PyObject* args, PyObject* kwargs) { + xmlSecKeysMngrPtr handle = xmlSecKeysMngrCreate(); + + PYXMLSEC_DEBUGF("%p: init key manager", self); + if (handle == NULL) { + PyXmlSec_SetLastError("failed to create xmlsecKeyManager"); + return -1; + } + if (xmlSecCryptoAppDefaultKeysMngrInit(handle) < 0) { + xmlSecKeysMngrDestroy(handle); + PyXmlSec_SetLastError("failed to initialize xmlsecKeyManager"); + return -1; + } + PYXMLSEC_DEBUGF("%p: init key manager - done: %p", self, handle); + ((PyXmlSec_KeysManager*)self)->handle = handle; + return 0; +} + +static void PyXmlSec_KeysManager__del__(PyObject* self) { + PyXmlSec_KeysManager* mgr = (PyXmlSec_KeysManager*)self; + + PYXMLSEC_DEBUGF("%p: delete KeysManager", self); + + 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__[] = \ + "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_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)) { + goto ON_FAIL; + } + + if (key->handle == NULL) { + PyErr_SetString(PyExc_ValueError, "the provided key is invalid"); + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS + key2 = xmlSecKeyDuplicate(key->handle); + Py_END_ALLOW_THREADS; + + if (key2 == NULL) { + PyXmlSec_SetLastError("cannot make copy of key"); + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + rv = xmlSecCryptoAppDefaultKeysMngrAdoptKey(mgr->handle, key2); + Py_END_ALLOW_THREADS; + if (rv < 0) { + PyXmlSec_SetLastError("cannot add key"); + xmlSecKeyDestroy(key2); + goto ON_FAIL; + } + PYXMLSEC_DEBUGF("%p: add key - ok", self); + Py_RETURN_NONE; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: add key - fail", self); + return NULL; +} + +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}; + + 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, "O&II:load_cert", kwlist, + PyUnicode_FSConverter, &filepath, &format, &type)) { + goto ON_FAIL; + } + + filename = PyBytes_AsString(filepath); + + Py_BEGIN_ALLOW_THREADS; + 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_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; + } + + Py_BEGIN_ALLOW_THREADS; + 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"); + goto ON_FAIL; + } + PYXMLSEC_DEBUGF("%p: load cert from memory - ok", self); + Py_RETURN_NONE; +ON_FAIL: + PYXMLSEC_DEBUGF("%p: load cert from memory - fail", self); + return NULL; +} + +static PyMethodDef PyXmlSec_KeysManagerMethods[] = { + { + "add_key", + (PyCFunction)PyXmlSec_KeysManagerAddKey, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_KeysManagerAddKey__doc__ + }, + { + "load_cert", + (PyCFunction)PyXmlSec_KeysManagerLoadCert, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_KeysManagerLoadCert__doc__ + }, + { + "load_cert_from_memory", + (PyCFunction)PyXmlSec_KeysManagerLoadCertFromMemory, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_KeysManagerLoadCertFromMemory__doc__ + }, + {NULL, NULL} /* sentinel */ +}; + +static PyTypeObject _PyXmlSec_KeysManagerType = { + PyVarObject_HEAD_INIT(NULL, 0) + STRINGIFY(MODULE_NAME) ".KeysManager", /* tp_name */ + sizeof(PyXmlSec_KeysManager), /* tp_basicsize */ + 0, /* tp_itemsize */ + PyXmlSec_KeysManager__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|Py_TPFLAGS_BASETYPE, /* tp_flags */ + "Keys Manager", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + PyXmlSec_KeysManagerMethods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + PyXmlSec_KeysManager__init__, /* tp_init */ + 0, /* tp_alloc */ + PyXmlSec_KeysManager__new__, /* tp_new */ + 0, /* tp_free */ +}; + +PyTypeObject* PyXmlSec_KeysManagerType = &_PyXmlSec_KeysManagerType; + + +int PyXmlSec_KeysManagerConvert(PyObject* o, PyXmlSec_KeysManager** p) { + if (o == Py_None) { + *p = NULL; + return 1; + } + if (!PyObject_IsInstance(o, (PyObject*)PyXmlSec_KeysManagerType)) { + PyErr_SetString(PyExc_TypeError, "KeysManager required"); + return 0; + } + *p = (PyXmlSec_KeysManager*)(o); + Py_INCREF(o); + return 1; +} + +int PyXmlSec_KeyModule_Init(PyObject* package) { + if (PyType_Ready(PyXmlSec_KeyType) < 0) goto ON_FAIL; + if (PyType_Ready(PyXmlSec_KeysManagerType) < 0) goto ON_FAIL; + + // since objects is created as static objects, need to increase refcount to prevent deallocate + Py_INCREF(PyXmlSec_KeyType); + Py_INCREF(PyXmlSec_KeysManagerType); + + if (PyModule_AddObject(package, "Key", (PyObject*)PyXmlSec_KeyType) < 0) goto ON_FAIL; + if (PyModule_AddObject(package, "KeysManager", (PyObject*)PyXmlSec_KeysManagerType) < 0) goto ON_FAIL; + + return 0; +ON_FAIL: + return -1; +} diff --git a/src/keys.h b/src/keys.h new file mode 100644 index 00000000..546f7a10 --- /dev/null +++ b/src/keys.h @@ -0,0 +1,37 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __PYXMLSEC_KEY_H__ +#define __PYXMLSEC_KEY_H__ + +#include "platform.h" + +#include + +typedef struct { + PyObject_HEAD + xmlSecKeyPtr handle; + int is_own; +} PyXmlSec_Key; + +extern PyTypeObject* PyXmlSec_KeyType; + +PyXmlSec_Key* PyXmlSec_NewKey(void); + +typedef struct { + PyObject_HEAD + xmlSecKeysMngrPtr handle; +} PyXmlSec_KeysManager; + +extern PyTypeObject* PyXmlSec_KeysManagerType; + +// converts object `o` to PyXmlSec_KeysManager, None will be converted to NULL, increments ref_count +int PyXmlSec_KeysManagerConvert(PyObject* o, PyXmlSec_KeysManager** p); + +#endif //__PYXMLSEC_KEY_H__ diff --git a/src/lxml.c b/src/lxml.c new file mode 100644 index 00000000..c98e933b --- /dev/null +++ b/src/lxml.c @@ -0,0 +1,131 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "common.h" +#include "lxml.h" +#include "exception.h" + +#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); +} + + +int PyXmlSec_LxmlElementConverter(PyObject* o, PyXmlSec_LxmlElementPtr* p) { + PyXmlSec_LxmlElementPtr node = rootNodeOrRaise(o); + if (node == NULL) { + return 0; + } + *p = node; + // rootNodeOrRaise - increments ref-count, so need to compensate this. + Py_DECREF(node); + return 1; +} diff --git a/src/lxml.h b/src/lxml.h new file mode 100644 index 00000000..72050efe --- /dev/null +++ b/src/lxml.h @@ -0,0 +1,41 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __PYXMLSEC_LXML_H__ +#define __PYXMLSEC_LXML_H__ + +#include "platform.h" + +#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 new file mode 100644 index 00000000..61eac139 --- /dev/null +++ b/src/main.c @@ -0,0 +1,540 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "common.h" +#include "platform.h" +#include "exception.h" +#include "lxml.h" + +#include +#include +#include +#include +#include + +#define _PYXMLSEC_FREE_NONE 0 +#define _PYXMLSEC_FREE_XMLSEC 1 +#define _PYXMLSEC_FREE_CRYPTOLIB 2 +#define _PYXMLSEC_FREE_ALL 3 + +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 _PYXMLSEC_FREE_ALL: + xmlSecCryptoAppShutdown(); + case _PYXMLSEC_FREE_CRYPTOLIB: +#ifndef XMLSEC_NO_CRYPTO_DYNAMIC_LOADING + xmlSecCryptoDLUnloadLibrary(PyXmlSec_GetCryptoLibName()); +#endif + case _PYXMLSEC_FREE_XMLSEC: + xmlSecShutdown(); + } + free_mode = _PYXMLSEC_FREE_NONE; +} + +static int PyXmlSec_Init(void) { + if (xmlSecInit() < 0) { + PyXmlSec_SetLastError("cannot initialize xmlsec library."); + PyXmlSec_Free(_PYXMLSEC_FREE_NONE); + return -1; + } + + if (xmlSecCheckVersion() != 1) { + PyXmlSec_SetLastError("xmlsec library version mismatch."); + PyXmlSec_Free(_PYXMLSEC_FREE_XMLSEC); + return -1; + } + +#ifndef XMLSEC_NO_CRYPTO_DYNAMIC_LOADING + if (xmlSecCryptoDLLoadLibrary(PyXmlSec_GetCryptoLibName()) < 0) { + PyXmlSec_SetLastError("cannot load crypto library for xmlsec."); + PyXmlSec_Free(_PYXMLSEC_FREE_XMLSEC); + return -1; + } +#endif /* XMLSEC_CRYPTO_DYNAMIC_LOADING */ + + /* Init crypto library */ + if (xmlSecCryptoAppInit(NULL) < 0) { + PyXmlSec_SetLastError("cannot initialize crypto library application."); + PyXmlSec_Free(_PYXMLSEC_FREE_CRYPTOLIB); + return -1; + } + + /* Init xmlsec-crypto library */ + if (xmlSecCryptoInit() < 0) { + PyXmlSec_SetLastError("cannot initialize crypto library."); + PyXmlSec_Free(_PYXMLSEC_FREE_ALL); + return -1; + } + // xmlsec will install default callback in xmlSecCryptoInit, + // overwriting any custom callbacks. + // We thus reinstall our callback now. + PyXmlSec_InstallErrorCallback(); + + free_mode = _PYXMLSEC_FREE_ALL; + return 0; +} + +static char PyXmlSec_PyInit__doc__[] = \ + "init() -> None\n" + "Initializes the library for general operation.\n\n" + "This is called upon library import and does not need to be called\n" + "again :func:`~.shutdown` is called explicitly).\n"; +static PyObject* PyXmlSec_PyInit(PyObject *self) { + if (PyXmlSec_Init() < 0) { + return NULL; + } + Py_RETURN_NONE; +} + +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_mode); + Py_RETURN_NONE; +} + +static char PyXmlSec_GetLibXmlSecVersion__doc__[] = \ + "get_libxmlsec_version() -> tuple\n" + "Returns Version tuple of wrapped libxmlsec library."; +static PyObject* PyXmlSec_GetLibXmlSecVersion() { + return Py_BuildValue("(iii)", XMLSEC_VERSION_MAJOR, XMLSEC_VERSION_MINOR, XMLSEC_VERSION_SUBMINOR); +} + +static char PyXmlSec_GetLibXmlVersion__doc__[] = \ + "get_libxml_version() -> tuple[int, int, int]\n" + "Returns version tuple of libxml2 library xmlsec is using."; +static PyObject* PyXmlSec_GetLibXmlVersion() { + return Py_BuildValue( + "(iii)", + PyXmlSec_GetLibXmlVersionMajor(), + PyXmlSec_GetLibXmlVersionMinor(), + PyXmlSec_GetLibXmlVersionPatch() + ); +} + +static char PyXmlSec_GetLibXmlCompiledVersion__doc__[] = \ + "get_libxml_compiled_version() -> tuple[int, int, int]\n" + "Returns version tuple of libxml2 library xmlsec was compiled with."; +static PyObject* PyXmlSec_GetLibXmlCompiledVersion() { + return Py_BuildValue( + "(iii)", + PyXmlSec_GetLibXmlCompiledVersionMajor(), + PyXmlSec_GetLibXmlCompiledVersionMinor(), + PyXmlSec_GetLibXmlCompiledVersionPatch() + ); +} + +static char PyXmlSec_PyEnableDebugOutput__doc__[] = \ + "enable_debug_trace(enabled) -> None\n" + "Enables or disables calling LibXML2 callback from the default errors callback.\n\n" + ":param 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; + } + 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; +} + +static PyMethodDef PyXmlSec_MainMethods[] = { + { + "init", + (PyCFunction)PyXmlSec_PyInit, + METH_NOARGS, + PyXmlSec_PyInit__doc__ + }, + { + "shutdown", + (PyCFunction)PyXmlSec_PyShutdown, + 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 */ +}; + +// modules entry points +// loads lxml module +int PyXmlSec_InitLxmlModule(void); +// constants +int PyXmlSec_ConstantsModule_Init(PyObject* package); +// exceptions +int PyXmlSec_ExceptionsModule_Init(PyObject* package); +// keys management +int PyXmlSec_KeyModule_Init(PyObject* package); +// init lxml.tree integration +int PyXmlSec_TreeModule_Init(PyObject* package); +// digital signature management +int PyXmlSec_DSModule_Init(PyObject* package); +// encryption management +int PyXmlSec_EncModule_Init(PyObject* package); +// templates management +int PyXmlSec_TemplateModule_Init(PyObject* package); + +static int PyXmlSec_PyClear(PyObject *self) { + PyXmlSec_Free(free_mode); + return 0; +} + +static PyModuleDef PyXmlSecModule = { + PyModuleDef_HEAD_INIT, + STRINGIFY(MODULE_NAME), /* name of module */ + 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 */ + NULL, /* m_slots */ + NULL, /* m_traverse */ + PyXmlSec_PyClear, /* m_clear */ + NULL, /* m_free */ +}; + +#define PYENTRY_FUNC_NAME JOIN(PyInit_, MODULE_NAME) +#define PY_MOD_RETURN(m) return m + +PyMODINIT_FUNC +PYENTRY_FUNC_NAME(void) +{ + PyObject *module = NULL; + module = PyModule_Create(&PyXmlSecModule); + 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; + + if (PyXmlSec_InitLxmlModule() < 0) goto ON_FAIL; + /* Populate final object settings */ + if (PyXmlSec_ConstantsModule_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; + + PY_MOD_RETURN(module); +ON_FAIL: + PY_MOD_RETURN(NULL); +} diff --git a/src/platform.h b/src/platform.h new file mode 100644 index 00000000..35163e88 --- /dev/null +++ b/src/platform.h @@ -0,0 +1,43 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __PYXMLSEC_PLATFORM_H__ +#define __PYXMLSEC_PLATFORM_H__ + +#define PY_SSIZE_T_CLEAN 1 + +#include +#include + +#ifdef MS_WIN32 +#include +#endif /* MS_WIN32 */ + +#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 > 0x10214 +#define XMLSEC_NO_XKMS 1 +#endif + +#define XSTR(c) (const xmlChar*)(c) + +#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) +typedef int Py_ssize_t; +#define PY_SSIZE_T_MAX INT_MAX +#define PY_SSIZE_T_MIN INT_MIN +#endif + +static inline char* PyBytes_AsStringAndSize2(PyObject *obj, Py_ssize_t* length) { + char* buffer = NULL; + return ((PyBytes_AsStringAndSize(obj, &buffer, length) < 0) ? (char*)(0) : buffer); +} + +#endif //__PYXMLSEC_PLATFORM_H__ diff --git a/src/template.c b/src/template.c new file mode 100644 index 00000000..ae0eca34 --- /dev/null +++ b/src/template.c @@ -0,0 +1,946 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "common.h" +#include "platform.h" +#include "exception.h" +#include "constants.h" +#include "lxml.h" + +#include + +#define PYXMLSEC_TEMPLATES_DOC "Xml Templates processing" + +static char PyXmlSec_TemplateCreate__doc__[] = \ + "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", "id", "ns", "name", NULL}; + + PyXmlSec_LxmlElementPtr node = NULL; + PyXmlSec_Transform* c14n = NULL; + PyXmlSec_Transform* sign = NULL; + const char* id = NULL; + const char* ns = NULL; + xmlNodePtr res; + + PYXMLSEC_DEBUG("template create - start"); + 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; + } + + Py_BEGIN_ALLOW_THREADS; + 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."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template create - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template create - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddReference__doc__[] = \ + "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}; + + PyXmlSec_LxmlElementPtr node = NULL; + PyXmlSec_Transform* digest = NULL; + 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, + PyXmlSec_LxmlElementConverter, &node, PyXmlSec_TransformType, &digest, &id, &uri, &type)) + { + goto ON_FAIL; + } + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplSignatureAddReference(node->_c_node, digest->id, XSTR(id), XSTR(uri), XSTR(type)); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add reference."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template add_reference - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template add_reference - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddTransform__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node, PyXmlSec_TransformType, &transform)) + { + goto ON_FAIL; + } + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplReferenceAddTransform(node->_c_node, transform->id); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add transform."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template add_transform - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template add_transform - fail"); + return NULL; +} + +static char PyXmlSec_TemplateEnsureKeyInfo__doc__[] = \ + "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; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplSignatureEnsureKeyInfo(node->_c_node, XSTR(id)); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot ensure key info."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template ensure_key_info - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template ensure_key_info - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddKeyName__doc__[] = \ + "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)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplKeyInfoAddKeyName(node->_c_node, XSTR(name)); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add key name."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template add_key_name - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template add_key_name - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddKeyValue__doc__[] = \ + "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)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplKeyInfoAddKeyValue(node->_c_node); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add key value."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template add_key_name - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template add_key_name - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddX509Data__doc__[] = \ + "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)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplKeyInfoAddX509Data(node->_c_node); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add x509 data."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template add_x509_data - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template add_x509_data - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddX509DataAddIssuerSerial__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node)) + { + goto ON_FAIL; + } + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplX509DataAddIssuerSerial(node->_c_node); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add x509 issuer serial."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template x509_data_add_issuer_serial - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template x509_data_add_issuer_serial - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerName__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node, &name)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplX509IssuerSerialAddIssuerName(node->_c_node, XSTR(name)); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add x509 issuer serial name."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template x509_issuer_serial_add_issuer_name - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template x509_issuer_serial_add_issuer_name - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerSerialNumber__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node, &serial)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplX509IssuerSerialAddSerialNumber(node->_c_node, XSTR(serial)); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add x509 issuer serial number."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template x509_issuer_serial_add_serial_number - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template x509_issuer_serial_add_serial_number - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddX509DataAddSubjectName__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplX509DataAddSubjectName(node->_c_node); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add x509 subject name."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template x509_data_add_subject_name - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template x509_data_add_subject_name - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddX509DataAddSKI__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplX509DataAddSKI(node->_c_node); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add x509 SKI."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template x509_data_add_ski - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template x509_data_add_ski - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddX509DataAddCertificate__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplX509DataAddCertificate(node->_c_node); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add x509 certificate."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template x509_data_add_certificate - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template x509_data_add_certificate - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddX509DataAddCRL__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplX509DataAddCRL(node->_c_node); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add x509 CRL."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template x509_data_add_crl - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template x509_data_add_crl - fail"); + return NULL; +} + +static char PyXmlSec_TemplateAddEncryptedKey__doc__[] = \ + "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}; + + PyXmlSec_LxmlElementPtr node = NULL; + PyXmlSec_Transform* method = NULL; + 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, + PyXmlSec_LxmlElementConverter, &node, PyXmlSec_TransformType, &method, &id, &type, &recipient)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplKeyInfoAddEncryptedKey(node->_c_node, method->id, XSTR(id), XSTR(type), XSTR(recipient)); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot add encrypted key."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template add_encrypted_key - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template add_encrypted_key - fail"); + return NULL; +} + +static char PyXmlSec_TemplateCreateEncryptedData__doc__[] = \ + "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}; + + PyXmlSec_LxmlElementPtr node = NULL; + PyXmlSec_Transform* method = NULL; + const char* id = NULL; + const char* type = NULL; + 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, + PyXmlSec_LxmlElementConverter, &node, PyXmlSec_TransformType, &method, &id, &type, &mime_type, &encoding, &ns)) + { + goto ON_FAIL; + } + + 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; + if (res == NULL) { + PyXmlSec_SetLastError("cannot create encrypted data."); + goto ON_FAIL; + } + if (ns != NULL) { + res->ns->prefix = xmlStrdup(XSTR(ns)); + } + + PYXMLSEC_DEBUG("template encrypted_data_create - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template encrypted_data_create - fail"); + return NULL; +} + +static char PyXmlSec_TemplateEncryptedDataEnsureKeyInfo__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node, &id, &ns)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplEncDataEnsureKeyInfo(node->_c_node, XSTR(id)); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot ensure key info for encrypted data."); + goto ON_FAIL; + } + if (ns != NULL) { + res->ns->prefix = xmlStrdup(XSTR(ns)); + } + + PYXMLSEC_DEBUG("template encrypted_data_ensure_key_info - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template encrypted_data_ensure_key_info - fail"); + return NULL; +} + +static char PyXmlSec_TemplateEncryptedDataEnsureCipherValue__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecTmplEncDataEnsureCipherValue(node->_c_node); + Py_END_ALLOW_THREADS; + if (res == NULL) { + PyXmlSec_SetLastError("cannot ensure cipher value for encrypted data."); + goto ON_FAIL; + } + + PYXMLSEC_DEBUG("template encrypted_data_ensure_cipher_value - ok"); + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("template encrypted_data_ensure_cipher_value - fail"); + 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[] = { + { + "create", + (PyCFunction)PyXmlSec_TemplateCreate, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateCreate__doc__ + }, + { + "add_reference", + (PyCFunction)PyXmlSec_TemplateAddReference, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddReference__doc__ + }, + { + "add_transform", + (PyCFunction)PyXmlSec_TemplateAddTransform, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddTransform__doc__ + }, + { + "ensure_key_info", + (PyCFunction)PyXmlSec_TemplateEnsureKeyInfo, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateEnsureKeyInfo__doc__ + }, + { + "add_key_name", + (PyCFunction)PyXmlSec_TemplateAddKeyName, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddKeyName__doc__ + }, + { + "add_key_value", + (PyCFunction)PyXmlSec_TemplateAddKeyValue, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddKeyValue__doc__ + }, + { + "add_x509_data", + (PyCFunction)PyXmlSec_TemplateAddX509Data, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddX509Data__doc__ + }, + { + "x509_data_add_issuer_serial", + (PyCFunction)PyXmlSec_TemplateAddX509DataAddIssuerSerial, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddX509DataAddIssuerSerial__doc__ + }, + { + "x509_issuer_serial_add_issuer_name", + (PyCFunction)PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerName, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerName__doc__ + }, + { + "x509_issuer_serial_add_serial_number", + (PyCFunction)PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerSerialNumber, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddX509DataIssuerSerialAddIssuerSerialNumber__doc__ + }, + { + "x509_data_add_subject_name", + (PyCFunction)PyXmlSec_TemplateAddX509DataAddSubjectName, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddX509DataAddSubjectName__doc__ + }, + { + "x509_data_add_ski", + (PyCFunction)PyXmlSec_TemplateAddX509DataAddSKI, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddX509DataAddSKI__doc__ + }, + { + "x509_data_add_certificate", + (PyCFunction)PyXmlSec_TemplateAddX509DataAddCertificate, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddX509DataAddCertificate__doc__ + }, + { + "x509_data_add_crl", + (PyCFunction)PyXmlSec_TemplateAddX509DataAddCRL, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddX509DataAddCRL__doc__ + }, + { + "add_encrypted_key", + (PyCFunction)PyXmlSec_TemplateAddEncryptedKey, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateAddEncryptedKey__doc__ + }, + { + "encrypted_data_create", + (PyCFunction)PyXmlSec_TemplateCreateEncryptedData, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateCreateEncryptedData__doc__ + }, + { + "encrypted_data_ensure_key_info", + (PyCFunction)PyXmlSec_TemplateEncryptedDataEnsureKeyInfo, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TemplateEncryptedDataEnsureKeyInfo__doc__ + }, + { + "encrypted_data_ensure_cipher_value", + (PyCFunction)PyXmlSec_TemplateEncryptedDataEnsureCipherValue, + 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 */ +}; + +static PyModuleDef PyXmlSec_TemplateModule = +{ + PyModuleDef_HEAD_INIT, + STRINGIFY(MODULE_NAME) ".template", + PYXMLSEC_TEMPLATES_DOC, + -1, + PyXmlSec_TemplateMethods, /* m_methods */ + NULL, /* m_slots */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL, /* m_free */ +}; + +int PyXmlSec_TemplateModule_Init(PyObject* package) { + PyObject* template = PyModule_Create(&PyXmlSec_TemplateModule); + + if (!template) goto ON_FAIL; + PYXMLSEC_DEBUGF("%p", template); + + if (PyModule_AddObject(package, "template", template) < 0) goto ON_FAIL; + + return 0; +ON_FAIL: + Py_XDECREF(template); + return -1; +} diff --git a/src/tree.c b/src/tree.c new file mode 100644 index 00000000..37cae785 --- /dev/null +++ b/src/tree.c @@ -0,0 +1,258 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "common.h" +#include "utils.h" +#include "lxml.h" + +#include + +#define PYXMLSEC_TREE_DOC "Common XML utility functions" + +static char PyXmlSec_TreeFindChild__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node, &name, &ns)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecFindChild(node->_c_node, XSTR(name), XSTR(ns)); + Py_END_ALLOW_THREADS; + + PYXMLSEC_DEBUG("tree find_child - ok"); + if (res == NULL) { + Py_RETURN_NONE; + } + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("tree find_child - fail"); + return NULL; +} + +static char PyXmlSec_TreeFindParent__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node, &name, &ns)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecFindParent(node->_c_node, XSTR(name), XSTR(ns)); + Py_END_ALLOW_THREADS; + + PYXMLSEC_DEBUG("tree find_parent - ok"); + if (res == NULL) { + Py_RETURN_NONE; + } + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("tree find_parent - fail"); + return NULL; +} + +static char PyXmlSec_TreeFindNode__doc__[] = \ + "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, + PyXmlSec_LxmlElementConverter, &node, &name, &ns)) + { + goto ON_FAIL; + } + + Py_BEGIN_ALLOW_THREADS; + res = xmlSecFindNode(node->_c_node, XSTR(name), XSTR(ns)); + Py_END_ALLOW_THREADS; + + PYXMLSEC_DEBUG("tree find_node - ok"); + if (res == NULL) { + Py_RETURN_NONE; + } + return (PyObject*)PyXmlSec_elementFactory(node->_doc, res); + +ON_FAIL: + PYXMLSEC_DEBUG("tree find_node - fail"); + return NULL; +} + +static char PyXmlSec_TreeAddIds__doc__[] = \ + "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}; + + PyXmlSec_LxmlElementPtr node = NULL; + PyObject* ids = NULL; + + 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; + } + n = PyObject_Length(ids); + if (n < 0) goto ON_FAIL; + + list = (const xmlChar**)xmlMalloc(sizeof(xmlChar*) * (n + 1)); + if (list == NULL) { + PyErr_SetString(PyExc_MemoryError, "no memory"); + goto ON_FAIL; + } + + 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(PyUnicode_AsUTF8(tmp)); + Py_DECREF(tmp); + if (list[i] == NULL) goto ON_FAIL; + } + list[n] = NULL; + + Py_BEGIN_ALLOW_THREADS; + xmlSecAddIDs(node->_doc->_c_doc, node->_c_node, list); + Py_END_ALLOW_THREADS; + + PyMem_Free(list); + + PYXMLSEC_DEBUG("tree add_ids - ok"); + Py_RETURN_NONE; +ON_FAIL: + PYXMLSEC_DEBUG("tree add_ids - fail"); + xmlFree(list); + return NULL; +} + +static PyMethodDef PyXmlSec_TreeMethods[] = { + { + "find_child", + (PyCFunction)PyXmlSec_TreeFindChild, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TreeFindChild__doc__, + }, + { + "find_parent", + (PyCFunction)PyXmlSec_TreeFindParent, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TreeFindParent__doc__, + }, + { + "find_node", + (PyCFunction)PyXmlSec_TreeFindNode, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TreeFindNode__doc__, + }, + { + "add_ids", + (PyCFunction)PyXmlSec_TreeAddIds, + METH_VARARGS|METH_KEYWORDS, + PyXmlSec_TreeAddIds__doc__, + }, + {NULL, NULL} /* sentinel */ +}; + +static PyModuleDef PyXmlSec_TreeModule = +{ + PyModuleDef_HEAD_INIT, + STRINGIFY(MODULE_NAME) ".tree", + PYXMLSEC_TREE_DOC, + -1, + PyXmlSec_TreeMethods, /* m_methods */ + NULL, /* m_slots */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL, /* m_free */ +}; + + +int PyXmlSec_TreeModule_Init(PyObject* package) { + PyObject* tree = PyModule_Create(&PyXmlSec_TreeModule); + + if (!tree) goto ON_FAIL; + + if (PyModule_AddObject(package, "tree", tree) < 0) goto ON_FAIL; + + return 0; +ON_FAIL: + Py_XDECREF(tree); + return -1; +} diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 00000000..cdcb182b --- /dev/null +++ b/src/utils.c @@ -0,0 +1,56 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "utils.h" + +PyObject* PyXmlSec_GetFilePathOrContent(PyObject* file, int* is_content) { + PyObject* data; + PyObject* utf8; + PyObject* tmp = NULL; + + if (PyObject_HasAttrString(file, "read")) { + data = PyObject_CallMethod(file, "read", NULL); + if (data != NULL && PyUnicode_Check(data)) { + utf8 = PyUnicode_AsUTF8String(data); + Py_DECREF(data); + data = utf8; + } + *is_content = 1; + return data; + } + *is_content = 0; + if (!PyUnicode_FSConverter(file, &tmp)) { + return NULL; + } + return tmp; +} + +int PyXmlSec_SetStringAttr(PyObject* obj, const char* name, const char* value) { + PyObject* tmp = PyUnicode_FromString(value); + int r; + + if (tmp == NULL) { + return -1; + } + 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; + } + r = PyObject_SetAttrString(obj, name, tmp); + Py_DECREF(tmp); + return r; +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 00000000..a250ccbc --- /dev/null +++ b/src/utils.h @@ -0,0 +1,22 @@ +// Copyright (c) 2017 Ryan Leckey +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __PYXMLSEC_UTILS_H__ +#define __PYXMLSEC_UTILS_H__ + +#include "platform.h" + +int PyXmlSec_SetStringAttr(PyObject* obj, const char* name, const char* value); + +int PyXmlSec_SetLongAttr(PyObject* obj, const char* name, long value); + +// return content if file is fileobject, or fs encoded filepath +PyObject* PyXmlSec_GetFilePathOrContent(PyObject* file, int* is_content); + +#endif //__PYXMLSEC_UTILS_H__ diff --git a/src/xmlsec.h b/src/xmlsec.h deleted file mode 100644 index 6d741375..00000000 --- a/src/xmlsec.h +++ /dev/null @@ -1,6 +0,0 @@ -#include -#include -#include -#include -#include -#include diff --git a/src/xmlsec/__init__.py b/src/xmlsec/__init__.py deleted file mode 100644 index 106b1f00..00000000 --- a/src/xmlsec/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division -import atexit -from .meta import version as __version__, description as __doc__ -from .constants import * -from .utils import * -from .key import * -from .ds import * -from .enc import * -from .error import * -from . import tree, template - - -if not init(): - raise RuntimeError('Failed to initialize the xmlsec library.') - - -atexit.register(shutdown) 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.pxd b/src/xmlsec/constants.pxd deleted file mode 100644 index 5fa20306..00000000 --- a/src/xmlsec/constants.pxd +++ /dev/null @@ -1,119 +0,0 @@ -from lxml.includes.tree cimport const_xmlChar - - -cdef extern from "xmlsec.h": # xmlsec/strings.h - # Global namespaces. - const_xmlChar* xmlSecNs - const_xmlChar* xmlSecDSigNs - const_xmlChar* xmlSecEncNs - const_xmlChar* xmlSecXkmsNs - const_xmlChar* xmlSecXPathNs - const_xmlChar* xmlSecXPath2Ns - const_xmlChar* xmlSecXPointerNs - const_xmlChar* xmlSecSoap11Ns - const_xmlChar* xmlSecSoap12Ns - - # Digital signature nodes. - const_xmlChar* xmlSecNodeSignature - const_xmlChar* xmlSecNodeSignedInfo - const_xmlChar* xmlSecNodeCanonicalizationMethod - const_xmlChar* xmlSecNodeSignatureMethod - const_xmlChar* xmlSecNodeSignatureValue - const_xmlChar* xmlSecNodeDigestMethod - const_xmlChar* xmlSecNodeDigestValue - const_xmlChar* xmlSecNodeObject - const_xmlChar* xmlSecNodeManifest - const_xmlChar* xmlSecNodeSignatureProperties - - # Encypted nodes - const_xmlChar* xmlSecNodeEncryptedData - const_xmlChar* xmlSecNodeEncryptedKey - const_xmlChar* xmlSecNodeEncryptionMethod - const_xmlChar* xmlSecNodeEncryptionProperties - const_xmlChar* xmlSecNodeEncryptionProperty - const_xmlChar* xmlSecNodeCipherData - const_xmlChar* xmlSecNodeCipherValue - const_xmlChar* xmlSecNodeCipherReference - const_xmlChar* xmlSecNodeReferenceList - const_xmlChar* xmlSecNodeDataReference - const_xmlChar* xmlSecNodeKeyReference - const_xmlChar* xmlSecNodeKeyInfo - - - # encryption types - const_xmlChar* xmlSecTypeEncContent - const_xmlChar* xmlSecTypeEncElement - - ctypedef unsigned int xmlSecTransformUsage - cdef enum: - xmlSecTransformUsageUnknown=0x0000 - xmlSecTransformUsageDSigTransform=0x0001 - xmlSecTransformUsageC14NMethod=0x0002 - xmlSecTransformUsageDigestMethod=0x0004 - xmlSecTransformUsageSignatureMethod=0x0008 - xmlSecTransformUsageEncryptionMethod=0x0010 - xmlSecTransformUsageAny=0xFFFF - - # Transform ids - cdef struct _xmlSecTransformKlass: - const_xmlChar* name - const_xmlChar* href - xmlSecTransformUsage usage - - ctypedef _xmlSecTransformKlass *xmlSecTransformId - - xmlSecTransformId xmlSecTransformInclC14NId - xmlSecTransformId xmlSecTransformInclC14NWithCommentsId - xmlSecTransformId xmlSecTransformInclC14N11Id - xmlSecTransformId xmlSecTransformInclC14N11WithCommentsId - xmlSecTransformId xmlSecTransformExclC14NId - xmlSecTransformId xmlSecTransformExclC14NWithCommentsId - xmlSecTransformId xmlSecTransformEnvelopedId - xmlSecTransformId xmlSecTransformXPathId - xmlSecTransformId xmlSecTransformXPath2Id - xmlSecTransformId xmlSecTransformXPointerId - xmlSecTransformId xmlSecTransformXsltId - xmlSecTransformId xmlSecTransformRemoveXmlTagsC14NId - xmlSecTransformId xmlSecTransformVisa3DHackId - - xmlSecTransformId xmlSecTransformAes128CbcId - xmlSecTransformId xmlSecTransformAes192CbcId - xmlSecTransformId xmlSecTransformAes256CbcId - xmlSecTransformId xmlSecTransformKWAes128Id - xmlSecTransformId xmlSecTransformKWAes192Id - xmlSecTransformId xmlSecTransformKWAes256Id - xmlSecTransformId xmlSecTransformDes3CbcId - xmlSecTransformId xmlSecTransformKWDes3Id - xmlSecTransformId xmlSecTransformDsaSha1Id - xmlSecTransformId xmlSecTransformEcdsaSha1Id - xmlSecTransformId xmlSecTransformEcdsaSha224Id - xmlSecTransformId xmlSecTransformEcdsaSha256Id - xmlSecTransformId xmlSecTransformEcdsaSha384Id - xmlSecTransformId xmlSecTransformEcdsaSha512Id - xmlSecTransformId xmlSecTransformHmacMd5Id - xmlSecTransformId xmlSecTransformHmacRipemd160Id - xmlSecTransformId xmlSecTransformHmacSha1Id - xmlSecTransformId xmlSecTransformHmacSha224Id - xmlSecTransformId xmlSecTransformHmacSha256Id - xmlSecTransformId xmlSecTransformHmacSha384Id - xmlSecTransformId xmlSecTransformHmacSha512Id - xmlSecTransformId xmlSecTransformMd5Id - xmlSecTransformId xmlSecTransformRipemd160Id - xmlSecTransformId xmlSecTransformRsaMd5Id - xmlSecTransformId xmlSecTransformRsaRipemd160Id - xmlSecTransformId xmlSecTransformRsaSha1Id - xmlSecTransformId xmlSecTransformRsaSha224Id - xmlSecTransformId xmlSecTransformRsaSha256Id - xmlSecTransformId xmlSecTransformRsaSha384Id - xmlSecTransformId xmlSecTransformRsaSha512Id - xmlSecTransformId xmlSecTransformRsaPkcs1Id - xmlSecTransformId xmlSecTransformRsaOaepId - xmlSecTransformId xmlSecTransformSha1Id - xmlSecTransformId xmlSecTransformSha224Id - xmlSecTransformId xmlSecTransformSha256Id - xmlSecTransformId xmlSecTransformSha384Id - xmlSecTransformId xmlSecTransformSha512Id - - -cdef class _Transform(object): - cdef xmlSecTransformId target 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/constants.pyx b/src/xmlsec/constants.pyx deleted file mode 100644 index 91b6b06a..00000000 --- a/src/xmlsec/constants.pyx +++ /dev/null @@ -1,132 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division - -from .constants cimport * -from .utils cimport * - -__all__ = [ - 'Namespace', - 'Node', - 'Transform', - 'EncryptionType' -] - - -class Namespace: - """Global namespaces.""" - BASE = _u(xmlSecNs) - DS = _u(xmlSecDSigNs) - ENC = _u(xmlSecEncNs) - XKMS = _u(xmlSecXkmsNs) - XPATH = _u(xmlSecXPathNs) - XPATH2 = _u(xmlSecXPath2Ns) - XPOINTER = _u(xmlSecXPointerNs) - SOAP11 = _u(xmlSecSoap11Ns) - SOAP12 = _u(xmlSecSoap12Ns) - - -class EncryptionType: - CONTENT = _u(xmlSecTypeEncContent) - ELEMENT = _u(xmlSecTypeEncElement) - - -class Node: - """Digital signature nodes.""" - SIGNATURE = _u(xmlSecNodeSignature) - SIGNED_INFO = _u(xmlSecNodeSignedInfo) - CANONICALIZATION_METHOD = _u(xmlSecNodeCanonicalizationMethod) - SIGNATURE_METHOD = _u(xmlSecNodeSignatureMethod) - SIGNATURE_VALUE = _u(xmlSecNodeSignatureValue) - DIGEST_METHOD = _u(xmlSecNodeDigestMethod) - DIGEST_VALUE = _u(xmlSecNodeDigestValue) - OBJECT = _u(xmlSecNodeObject) - MANIFEST = _u(xmlSecNodeManifest) - SIGNATURE_PROPERTIES = _u(xmlSecNodeSignatureProperties) - ENCRYPTED_DATA = _u(xmlSecNodeEncryptedData) - ENCRYPTED_KEY = _u(xmlSecNodeEncryptedKey) - ENCRYPTION_METHOD = _u(xmlSecNodeEncryptionMethod) - ENCRYPTION_PROPERTIES = _u(xmlSecNodeEncryptionProperties) - ENCRYPTION_PROPERTY = _u(xmlSecNodeEncryptionProperty) - CIPHER_DATA = _u(xmlSecNodeCipherData) - CIPHER_VALUE = _u(xmlSecNodeCipherValue) - CIPHER_REFERENCE = _u(xmlSecNodeCipherReference) - REFERENCE_LIST = _u(xmlSecNodeReferenceList) - DATA_REFERENCE = _u(xmlSecNodeDataReference) - KEY_REFERENCE = _u(xmlSecNodeKeyReference) - KEY_INFO = _u(xmlSecNodeKeyInfo) - - -cdef class _Transform: - property name: - def __get__(self): - return _u(self.target.name) - - property href: - def __get__(self): - return _u(self.target.href) - - property usage: - def __get__(self): - return self.target.usage - - -cdef _Transform _mkti(xmlSecTransformId target): - cdef _Transform r = _Transform.__new__(_Transform) - r.target = target - return r - - -class Transform: - """Transform identifiers.""" - - C14N = _mkti(xmlSecTransformInclC14NId) - C14N_COMMENTS = _mkti(xmlSecTransformInclC14NWithCommentsId) - C14N11 = _mkti(xmlSecTransformInclC14N11Id) - C14N11_COMMENTS = _mkti(xmlSecTransformInclC14N11WithCommentsId) - EXCL_C14N = _mkti(xmlSecTransformExclC14NId) - EXCL_C14N_COMMENTS = _mkti(xmlSecTransformExclC14NWithCommentsId) - ENVELOPED = _mkti(xmlSecTransformEnvelopedId) - XPATH = _mkti(xmlSecTransformXPathId) - XPATH2 = _mkti(xmlSecTransformXPath2Id) - XPOINTER = _mkti(xmlSecTransformXPointerId) - XSLT = _mkti(xmlSecTransformXsltId) - REMOVE_XML_TAGS_C14N = _mkti(xmlSecTransformRemoveXmlTagsC14NId) - VISA3D_HACK = _mkti(xmlSecTransformVisa3DHackId) - - AES128 = _mkti(xmlSecTransformAes128CbcId) - AES192 = _mkti(xmlSecTransformAes192CbcId) - AES256 = _mkti(xmlSecTransformAes256CbcId) - KW_AES128 = _mkti(xmlSecTransformKWAes128Id) - KW_AES192 = _mkti(xmlSecTransformKWAes192Id) - KW_AES256 = _mkti(xmlSecTransformKWAes256Id) - DES3 = _mkti(xmlSecTransformDes3CbcId) - KW_DES3 = _mkti(xmlSecTransformKWDes3Id) - DSA_SHA1 = _mkti(xmlSecTransformDsaSha1Id) - ECDSA_SHA1 = _mkti(xmlSecTransformEcdsaSha1Id) - ECDSA_SHA224 = _mkti(xmlSecTransformEcdsaSha224Id) - ECDSA_SHA256 = _mkti(xmlSecTransformEcdsaSha256Id) - ECDSA_SHA384 = _mkti(xmlSecTransformEcdsaSha384Id) - ECDSA_SHA512 = _mkti(xmlSecTransformEcdsaSha512Id) - HMAC_MD5 = _mkti(xmlSecTransformHmacMd5Id) - HMAC_RIPEMD160 = _mkti(xmlSecTransformHmacRipemd160Id) - HMAC_SHA1 = _mkti(xmlSecTransformHmacSha1Id) - HMAC_SHA224 = _mkti(xmlSecTransformHmacSha224Id) - HMAC_SHA256 = _mkti(xmlSecTransformHmacSha256Id) - HMAC_SHA384 = _mkti(xmlSecTransformHmacSha384Id) - HMAC_SHA512 = _mkti(xmlSecTransformHmacSha512Id) - MD5 = _mkti(xmlSecTransformMd5Id) - RIPEMD160 = _mkti(xmlSecTransformRipemd160Id) - RSA_MD5 = _mkti(xmlSecTransformRsaMd5Id) - RSA_RIPEMD160 = _mkti(xmlSecTransformRsaRipemd160Id) - RSA_SHA1 = _mkti(xmlSecTransformRsaSha1Id) - RSA_SHA224 = _mkti(xmlSecTransformRsaSha224Id) - RSA_SHA256 = _mkti(xmlSecTransformRsaSha256Id) - RSA_SHA384 = _mkti(xmlSecTransformRsaSha384Id) - RSA_SHA512 = _mkti(xmlSecTransformRsaSha512Id) - RSA_PKCS1 = _mkti(xmlSecTransformRsaPkcs1Id) - RSA_OAEP = _mkti(xmlSecTransformRsaOaepId) - SHA1 = _mkti(xmlSecTransformSha1Id) - SHA224 = _mkti(xmlSecTransformSha224Id) - SHA256 = _mkti(xmlSecTransformSha256Id) - SHA384 = _mkti(xmlSecTransformSha384Id) - SHA512 = _mkti(xmlSecTransformSha512Id) diff --git a/src/xmlsec/ds.pxd b/src/xmlsec/ds.pxd deleted file mode 100644 index e8096677..00000000 --- a/src/xmlsec/ds.pxd +++ /dev/null @@ -1,125 +0,0 @@ -from lxml.includes.tree cimport xmlNode -from lxml.includes.tree cimport const_xmlChar, xmlNode, xmlID, xmlDoc, xmlAttr -from lxml.includes.dtdvalid cimport xmlValidCtxt -from .key cimport xmlSecKeyPtr, xmlSecKeyReq, xmlSecKeyReqPtr, xmlSecKeysMngrPtr -from .constants cimport xmlSecTransformId - - -cdef extern from "xmlsec.h": # xmlsec/keys.h - - ctypedef unsigned int xmlSecSize - ctypedef unsigned char xmlSecByte - ctypedef xmlSecByte const_xmlSecByte "const xmlSecByte" - - ctypedef void * xmlSecPtrList - ctypedef xmlSecPtrList * xmlSecPtrListPtr - ctypedef void * xmlSecPtr - - int xmlSecPtrListAdd(xmlSecPtrListPtr, xmlSecPtr) nogil - - int xmlSecPtrListEmpty(xmlSecPtrListPtr) nogil - - cdef struct _xmlSecBuffer: - xmlSecByte* data - size_t size - # size_t maxSize - # xmlSecAllocMode allocMode - - ctypedef _xmlSecBuffer *xmlSecBufferPtr - - # transforms and transform contexts (partial) - ctypedef enum xmlSecTransformStatus: - xmlSecTransformStatusNone = 0 - xmlSecTransformStatusWorking - xmlSecTransformStatusFinished - xmlSecTransformStatusOk - xmlSecTransformStatusFail - - ctypedef enum xmlSecTransformOperation: - xmlSecTransformOperationNone = 0 - xmlSecTransformOperationEncode - xmlSecTransformOperationDecode - xmlSecTransformOperationSign - xmlSecTransformOperationVerify - xmlSecTransformOperationEncrypt - xmlSecTransformOperationDecrypt - - cdef struct _xmlSecTransform: - xmlSecTransformOperation operation - xmlSecTransformStatus status - - ctypedef _xmlSecTransform* xmlSecTransformPtr - - cdef struct _xmlSecTransformCtx: - xmlSecBufferPtr result - xmlSecTransformStatus status - - ctypedef _xmlSecTransformCtx xmlSecTransformCtx - ctypedef _xmlSecTransformCtx* xmlSecTransformCtxPtr - - cdef struct xmlSecKeyInfoCtx: - xmlSecPtrList enabledKeyData - xmlSecKeyReq keyReq - - ctypedef enum xmlSecDSigStatus: - xmlSecDSigStatusUnknown = 0 - xmlSecDSigStatusSucceeded = 1 - xmlSecDSigStatusInvalid = 2 - - cdef struct _xmlSecDSigCtx: - # void* userData - # unsigned int flags - # unsigned int flags2 - xmlSecKeyInfoCtx keyInfoReadCtx - # xmlSecKeyInfoCtx keyInfoWriteCtx - xmlSecTransformCtx transformCtx - # xmlSecTransformUriType enabledReferenceUris - # xmlSecPtrListPtr enabledReferenceTransforms - # xmlSecTransformCtxPreExecuteCallback referencePreExecuteCallback - # xmlSecTransformId defSignMethodId - # xmlSecTransformId defC14NMethodId - # xmlSecTransformId defDigestMethodId - xmlSecKeyPtr signKey - xmlSecTransformOperation operation - # xmlSecBufferPtr result - xmlSecDSigStatus status - xmlSecTransformPtr signMethod - # xmlSecTransformPtr c14nMethod - # xmlSecTransformPtr preSignMemBufMethod - # xmlNode* signValueNode - # xmlChar* id - # xmlSecPtrList signedInfoReferences - # xmlSecPtrList manifestReferences - # void* reserved0 - # void* reserved1 - - ctypedef _xmlSecDSigCtx* xmlSecDSigCtxPtr - - - xmlSecDSigCtxPtr xmlSecDSigCtxCreate(xmlSecKeysMngrPtr) nogil - - int xmlSecDSigCtxSign(xmlSecDSigCtxPtr, xmlNode*) nogil - - int xmlSecDSigCtxProcessSignatureNode(xmlSecDSigCtxPtr, xmlNode*) nogil - - int xmlSecDSigCtxVerify(xmlSecDSigCtxPtr, xmlNode*) nogil - - int xmlSecDSigCtxEnableReferenceTransform( - xmlSecDSigCtxPtr, xmlSecTransformId) nogil - - int xmlSecDSigCtxEnableSignatureTransform( - xmlSecDSigCtxPtr, xmlSecTransformId) nogil - - void xmlSecDSigCtxDestroy(xmlSecDSigCtxPtr) nogil - - xmlID* xmlAddID(xmlValidCtxt* ctx, xmlDoc* doc, const_xmlChar* value, xmlAttr* attr) - - xmlSecTransformPtr xmlSecTransformCtxCreateAndAppend(xmlSecTransformCtxPtr, xmlSecTransformId) nogil - - int xmlSecTransformSetKey(xmlSecTransformPtr, xmlSecKeyPtr) nogil - - int xmlSecTransformSetKeyReq(xmlSecTransformPtr, xmlSecKeyReqPtr) nogil - - int xmlSecTransformVerify(xmlSecTransformPtr, const_xmlSecByte*, xmlSecSize, xmlSecTransformCtxPtr) nogil - - int xmlSecTransformCtxBinaryExecute(xmlSecTransformCtxPtr, const_xmlSecByte*, xmlSecSize) nogil diff --git a/src/xmlsec/ds.pyx b/src/xmlsec/ds.pyx deleted file mode 100644 index 2922c971..00000000 --- a/src/xmlsec/ds.pyx +++ /dev/null @@ -1,193 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division - -from lxml.includes.tree cimport xmlHasProp, xmlAttr -from lxml.includes.etreepublic cimport import_lxml__etree -import_lxml__etree() - -from lxml.includes.etreepublic cimport _Element - -from .ds cimport * -from .constants cimport _Transform, xmlSecTransformUsageSignatureMethod -from .key cimport Key as _Key, KeysManager as _KeysManager, _KeyData, \ - xmlSecKeyDuplicate, xmlSecKeyMatch, xmlSecKeyDestroy -from .error import * -from .utils cimport _b - - -__all__ = [ - 'SignatureContext' -] - - -cdef class SignatureContext(object): - """Digital signature context. - """ - - cdef xmlSecDSigCtxPtr _handle - - def __cinit__(self, _KeysManager manager=None): - cdef xmlSecKeysMngrPtr _manager = manager._handle if manager is not None else NULL - cdef xmlSecDSigCtxPtr handle - handle = xmlSecDSigCtxCreate(_manager) - if handle == NULL: - raise InternalError('Failed to create the digital signature context.', -1) - - # Store the constructed context handle. - self._handle = handle - - def __dealloc__(self): - if self._handle != NULL: - xmlSecDSigCtxDestroy(self._handle) - - property key: - def __set__(self, _Key key): - if self._handle.signKey != NULL: - xmlSecKeyDestroy(self._handle.signKey) - - self._handle.signKey = xmlSecKeyDuplicate(key._handle) - if self._handle.signKey == NULL: - raise InternalError("failed to duplicate key", -1) - - def __get__(self): - cdef _Key instance = _Key.__new__(_Key) - instance._owner = False - instance._handle = self._handle.signKey - return instance - - def register_id(self, _Element node not None): - cdef xmlAttr* attr - - attr = xmlHasProp(node._c_node, _b("ID")) - value = node.attrib.get("ID") - - xmlAddID(NULL, node._doc._c_doc, _b(value), attr) - - def sign(self, _Element node not None): - """Sign according to the signature template. - """ - - cdef int rv - - rv = xmlSecDSigCtxSign(self._handle, node._c_node) - if rv != 0: - raise Error('sign failed with return value', rv) - - def verify(self, _Element node not None): - """Verify according to the signature template. - """ - - cdef int rv - - rv = xmlSecDSigCtxVerify(self._handle, node._c_node) - if rv != 0: - raise Error('verify failed with return value', rv) - - if self._handle.status != xmlSecDSigStatusSucceeded: - raise VerificationError('Signature verification failed', self._handle.status) - - def sign_binary(self, bytes data not None, _Transform algorithm not None): - """sign binary data *data* with *algorithm* and return the signature. - You must already have set the context's `signKey` (its value must - be compatible with *algorithm* and signature creation). - """ - - cdef xmlSecDSigCtxPtr context = self._handle - context.operation = xmlSecTransformOperationSign - self._binary(context, data, algorithm) - if context.transformCtx.status != xmlSecTransformStatusFinished: - raise Error("signing failed with transform status", context.transformCtx.status) - res = context.transformCtx.result - return (res.data)[:res.size] - - def verify_binary(self, bytes data not None, _Transform algorithm not None, bytes signature not None): - """Verify *signature* for *data* with *algorithm*. - You must already have set the context's `signKey` (its value must - be compatible with *algorithm* and signature verification). - """ - - cdef int rv - cdef xmlSecDSigCtxPtr context = self._handle - context.operation = xmlSecTransformOperationVerify - self._binary(context, data, algorithm) - rv = xmlSecTransformVerify(context.signMethod, - signature, - len(signature), - &context.transformCtx) - if rv != 0: - raise Error("Verification failed with return value", rv) - - if context.signMethod.status != xmlSecTransformStatusOk: - raise VerificationError("Signature verification failed", context.signMethod.status) - - cdef _binary(self, xmlSecDSigCtxPtr context, bytes data, _Transform algorithm): - """common helper used for `sign_binary` and `verify_binary`.""" - - cdef int rv - cdef const_xmlSecByte* c_data = data - cdef xmlSecSize c_size = len(data) - - if not (algorithm.target.usage & xmlSecTransformUsageSignatureMethod): - raise Error("improper signature algorithm") - - if context.signMethod != NULL: - raise Error("Signature context already used; it is designed for one use only") - - context.signMethod = xmlSecTransformCtxCreateAndAppend(&(context.transformCtx), algorithm.target) - - if context.signMethod == NULL: - raise Error("Could not create signature transform") - - context.signMethod.operation = context.operation - if context.signKey == NULL: - raise Error("signKey not yet set") - - xmlSecTransformSetKeyReq(context.signMethod, &(context.keyInfoReadCtx.keyReq)) - - rv = xmlSecKeyMatch(context.signKey, NULL, &(context.keyInfoReadCtx.keyReq)) - if rv != 1: - raise Error("inappropriate key type") - - rv = xmlSecTransformSetKey(context.signMethod, context.signKey) - if rv != 0: - raise Error("`xmlSecTransfromSetKey` failed", rv) - - context.transformCtx.result = NULL - context.transformCtx.status = xmlSecTransformStatusNone - - rv = xmlSecTransformCtxBinaryExecute(&(context.transformCtx), c_data, c_size) - - if rv != 0: - raise Error("transformation failed error value", rv) - - if context.transformCtx.status != xmlSecTransformStatusFinished: - raise Error("transformation failed with status", context.transformCtx.status) - - def enable_reference_transform(self, _Transform transform): - """enable use of *t* as reference transform. - Note: by default, all transforms are enabled. The first call of - `enableReferenceTransform` will switch to explicitely enabled transforms. - """ - - rv = xmlSecDSigCtxEnableReferenceTransform(self._handle, transform.target) - if rv < 0: - raise Error("enableReferenceTransform failed", rv) - - def enable_signature_transform(self, _Transform transform): - """enable use of *t* as signature transform. - Note: by default, all transforms are enabled. The first call of - `enableSignatureTransform` will switch to explicitely enabled transforms. - """ - - rv = xmlSecDSigCtxEnableSignatureTransform(self._handle, transform.target) - if rv < 0: - raise Error("enableSignatureTransform failed", rv) - - def set_enabled_key_data(self, keydata_list): - cdef _KeyData keydata - cdef xmlSecPtrListPtr enabled_list = &(self._handle.keyInfoReadCtx.enabledKeyData) - xmlSecPtrListEmpty(enabled_list) - for keydata in keydata_list: - rv = xmlSecPtrListAdd(enabled_list, keydata.target) - if rv < 0: - raise Error("setEnabledKeyData failed") diff --git a/src/xmlsec/enc.pxd b/src/xmlsec/enc.pxd deleted file mode 100644 index 2de629b1..00000000 --- a/src/xmlsec/enc.pxd +++ /dev/null @@ -1,57 +0,0 @@ -from lxml.includes.tree cimport xmlChar, const_xmlChar, xmlNode -from .key cimport xmlSecKeyPtr, xmlSecKeyReqPtr, xmlSecKeysMngrPtr -from .ds cimport const_xmlSecByte, xmlSecBufferPtr - - -cdef extern from "xmlsec.h": # xmlsec/keys.h - - unsigned int XMLSEC_ENC_RETURN_REPLACED_NODE - - cdef struct _xmlSecEncCtx: - # void * userData - unsigned int flags - # unsigned int flags2 - # xmlEncCtxMode mode - # xmlSecKeyInfoCtx keyInfoReadCtx - # xmlSecKeyInfoCtx keyInfoWriteCtx - # xmlSecTransformCtx transformCtx - # xmlSecTransformId defEncMethodId - xmlSecKeyPtr encKey - # xmlSecTransformOperation operation - xmlSecBufferPtr result - # int resultBase64Encoded - bint resultReplaced - # xmlSecTransformPtr encMethod - # xmlChar* id - # xmlChar* type - # xmlChar* mimeType - # xmlChar* encoding - # xmlChar* recipient - # xmlChar* carriedKeyName - # xmlNode* encDataNode - # xmlNode* encMethodNode - # xmlNode* keyInfoNode - # xmlNode* cipherValueNode - xmlNode* replacedNodeList - # void* reserved1 - - ctypedef _xmlSecEncCtx* xmlSecEncCtxPtr - - xmlSecEncCtxPtr xmlSecEncCtxCreate(xmlSecKeysMngrPtr) nogil - int xmlSecEncCtxInitialize(xmlSecEncCtxPtr encCtx, xmlSecKeysMngrPtr keysMngr) nogil - - void xmlSecEncCtxFinalize(xmlSecEncCtxPtr) nogil - void xmlSecEncCtxDestroy(xmlSecEncCtxPtr) nogil - - int xmlSecEncCtxBinaryEncrypt( - xmlSecEncCtxPtr, xmlNode*, const_xmlSecByte*, size_t) nogil - - int xmlSecEncCtxXmlEncrypt(xmlSecEncCtxPtr, xmlNode*, xmlNode*) nogil - - int xmlSecEncCtxUriEncrypt(xmlSecEncCtxPtr, xmlNode*, const_xmlChar*) nogil - - int xmlSecEncCtxDecrypt(xmlSecEncCtxPtr, xmlNode*) nogil - - void xmlSecErrorsSetCallback(void *callback) nogil - - int xmlSecKeyMatch(xmlSecKeyPtr, const_xmlChar*, xmlSecKeyReqPtr) diff --git a/src/xmlsec/enc.pyx b/src/xmlsec/enc.pyx deleted file mode 100644 index bf970af5..00000000 --- a/src/xmlsec/enc.pyx +++ /dev/null @@ -1,231 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division - -from lxml.includes.etreepublic cimport import_lxml__etree -import_lxml__etree() - -from lxml.includes.etreepublic cimport _Document, _Element, elementFactory -from lxml.includes.tree cimport xmlDocCopyNode, xmlFreeNode, xmlNode, xmlDoc, xmlDocGetRootElement - -from .enc cimport * -from .constants import EncryptionType -from .utils cimport * -from .key cimport Key as _Key, KeysManager as _KeysManager, _KeyData, \ - xmlSecKeyDuplicate, xmlSecKeyMatch, xmlSecKeyDestroy - -from .error import * -from copy import copy - - -__all__ = [ - 'EncryptionContext' -] - - -# we let `lxml` get rid of the subtree by wrapping *c_node* into a -# proxy and then releasing it again. -# Note: if referenced by `lxml`, nodes inside the subtree may lack -# necessary namespace daclarations. Hopefully, I can -# convince the `lxml` maintainers to provide a really safe -# `lxml_safe_unlink` function - -cdef inline int _lxml_safe_dealloc(_Document doc, xmlNode* c_node) except -1: - elementFactory(doc, c_node) - return 0 - - - -cdef class EncryptionContext: - """Encryption context.""" - - cdef xmlSecEncCtxPtr _handle - - def __cinit__(self, _KeysManager manager=None): - cdef xmlSecKeysMngrPtr _manager = manager._handle if manager is not None else NULL - cdef xmlSecEncCtxPtr handle = xmlSecEncCtxCreate(_manager) - if handle == NULL: - raise InternalError("failed to create encryption context") - - self._handle = handle - - def __dealloc__(self): - if self._handle != NULL: - xmlSecEncCtxDestroy(self._handle) - - property key: - def __set__(self, _Key key): - if self._handle.encKey != NULL: - xmlSecKeyDestroy(self._handle.encKey) - - self._handle.encKey = xmlSecKeyDuplicate(key._handle) - if self._handle.encKey == NULL: - raise InternalError("failed to duplicate key", -1) - - def __get__(self): - cdef _Key instance = _Key.__new__(_Key) - instance._owner = False - instance._handle = self._handle.encKey - return instance - - def encrypt_binary(self, _Element template not None, data): - """encrypt binary *data* according to `EncryptedData` template *template* - and return the resulting `EncryptedData` subtree. - Note: *template* is modified in place. - """ - cdef int rv - - # Data to bytes - cdef const_xmlSecByte* c_data = data - cdef size_t c_size = len(data) - - with nogil: - rv = xmlSecEncCtxBinaryEncrypt(self._handle, template._c_node, c_data, c_size) - if rv < 0: - raise Error("failed to encrypt binary", rv) - return template - - def encrypt_xml(self, _Element template not None, _Element node not None): - """encrpyt *node* using *template* and return the resulting `EncryptedData` element. - The `Type` attribute of *template* decides whether *node* itself is - encrypted (`http://www.w3.org/2001/04/xmlenc#Element`) - or its content (`http://www.w3.org/2001/04/xmlenc#Content`). - It must have one of these two - values (or an exception is raised). - The operation modifies the tree containing *node* in a way that - `lxml` references to or into this tree may see a surprising state. You - should no longer rely on them. Especially, you should use - `getroottree()` on the result to obtain the encrypted result tree. - """ - - cdef int rv - cdef xmlNode *n - cdef xmlNode *nn - cdef xmlSecEncCtxPtr context = self._handle - cdef xmlNode *c_node = template._c_node - - et = template.get("Type") - if et not in (EncryptionType.ELEMENT, EncryptionType.CONTENT): - raise Error("unsupported `Type` for `encryptXML` (must be `%s` or `%s`)" % (EncryptionType.ELEMENT, EncryptionType.CONTENT), et) - - # `xmlSecEncCtxEncrypt` expects *template* to belong to the document of *node* - # if this is not the case, we copy the `libxml2` subtree there. - - if template._doc._c_doc != node._doc._c_doc: - with nogil: - c_node = xmlDocCopyNode(c_node, node._doc._c_doc, 1) # recursive - if c_node == NULL: - raise MemoryError("could not copy template tree") - - # `xmlSecEncCtxXmlEncrypt` will replace the subtree rooted - # 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 - - context.flags = XMLSEC_ENC_RETURN_REPLACED_NODE - - with nogil: - rv = xmlSecEncCtxXmlEncrypt(context, c_node, node._c_node) - - # release the replaced nodes in a way safe for `lxml` - - n = context.replacedNodeList - - while n != NULL: - nn = n.next - _lxml_safe_dealloc(node._doc, n) - n = nn - - context.replacedNodeList = NULL - if rv < 0: - if c_node._private == NULL: - # template tree was copied; free it again. - # Note: if the problem happened late (e.g. a `MemoryError`) - # `c_node' might already be part of the result tree. - # In this case, memory corruption may result. But, - # we have an inconsistent state anyway and the probability - # should be very low. - - with nogil: - xmlFreeNode(c_node) # free formerly copied template subtree - - raise Error("failed to encrypt xml", rv) - - # `c_node` contains the resulting `EncryptedData` element. - return elementFactory(node._doc, c_node) - - def encrypt_uri(self, _Element template not None, uri not None): - """encrypt binary data obtained from *uri* according to *template*.""" - - cdef int rv - cdef const_xmlChar* c_uri = _b(uri) - - with nogil: - rv = xmlSecEncCtxUriEncrypt(self._handle, template._c_node, c_uri) - - if rv < 0: - raise Error("failed to encrypt uri", rv) - return template - - def decrypt(self, _Element node not None): - """decrypt *node* (an `EncryptedData` element) and return 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. - If the operation modifies the tree, - `lxml` references to or into this tree may see a surprising state. You - should no longer rely on them. Especially, you should use - `getroottree()` on the result to obtain the decrypted result tree. - """ - - cdef int rv - cdef xmlSecEncCtxPtr context = self._handle - cdef bint decrypt_content - cdef xmlNode *n - cdef xmlNode *nn - cdef xmlNode *c_root - cdef xmlSecBufferPtr result - - decrypt_content = node.get("Type") == EncryptionType.CONTENT - - # must provide sufficient context to find the decrypted node - parent = node.getparent() - - if parent is not None: - enc_index = parent.index(node) - - context.flags = XMLSEC_ENC_RETURN_REPLACED_NODE - - with nogil: - rv = xmlSecEncCtxDecrypt(context, node._c_node) - - # release the replaced nodes in a way safe for `lxml` - n = context.replacedNodeList - while n != NULL: - nn = n.next - _lxml_safe_dealloc(node._doc, n) - n = nn - - context.replacedNodeList = NULL - - if rv < 0: - raise Error("failed to decrypt", rv) - - if not context.resultReplaced: - # binary result - result = context.result - return (context.result.data)[:result.size] - # XML result - if parent is not None: - if decrypt_content: - return parent - else: - return parent[enc_index] - - # root has been replaced - c_root = xmlDocGetRootElement(node._doc._c_doc) - if c_root == NULL: - raise Error("decryption resulted in a non well formed document") - return elementFactory(node._doc, c_root) diff --git a/src/xmlsec/error.py b/src/xmlsec/error.py deleted file mode 100644 index 675d5173..00000000 --- a/src/xmlsec/error.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- - -__all__ = ["Error", "VerificationError", "InternalError"] - - -class Error(Exception): - pass - - -class InternalError(Error): - pass - - -class VerificationError(Error): - pass diff --git a/src/xmlsec/key.pxd b/src/xmlsec/key.pxd deleted file mode 100644 index 8b06f75c..00000000 --- a/src/xmlsec/key.pxd +++ /dev/null @@ -1,120 +0,0 @@ -from lxml.includes.tree cimport const_xmlChar - - -cdef extern from *: - ctypedef char const_char "const char" - ctypedef unsigned char const_unsigned_char "const unsigned char" - - -cdef extern from "xmlsec.h": # xmlsec/keys.h - - cdef struct _xmlSecKeyDataKlass: - const_xmlChar* name - const_xmlChar* href - - ctypedef _xmlSecKeyDataKlass *xmlSecKeyDataId - - xmlSecKeyDataId xmlSecKeyDataNameId - xmlSecKeyDataId xmlSecKeyDataValueId - xmlSecKeyDataId xmlSecKeyDataRetrievalMethodId - xmlSecKeyDataId xmlSecKeyDataEncryptedKeyId - xmlSecKeyDataId xmlSecKeyDataAesId - xmlSecKeyDataId xmlSecKeyDataDesId - xmlSecKeyDataId xmlSecKeyDataDsaId - xmlSecKeyDataId xmlSecKeyDataEcdsaId - xmlSecKeyDataId xmlSecKeyDataHmacId - xmlSecKeyDataId xmlSecKeyDataRsaId - xmlSecKeyDataId xmlSecKeyDataX509Id - xmlSecKeyDataId xmlSecKeyDataRawX509CertId - - ctypedef enum xmlSecKeyDataFormat: - xmlSecKeyDataFormatUnknown = 0 - xmlSecKeyDataFormatBinary = 1 - xmlSecKeyDataFormatPem = 2 - xmlSecKeyDataFormatDer = 3 - xmlSecKeyDataFormatPkcs8Pem = 4 - xmlSecKeyDataFormatPkcs8Der = 5 - xmlSecKeyDataFormatPkcs12 = 6 - xmlSecKeyDataFormatCertPem = 7 - xmlSecKeyDataFormatCertDer = 8 - - ctypedef unsigned int xmlSecKeyDataType - - cdef enum: - xmlSecKeyDataTypeUnknown = 0x0000 - xmlSecKeyDataTypeNone = 0x0000 - xmlSecKeyDataTypePublic = 0x0001 - xmlSecKeyDataTypePrivate = 0x0002 - xmlSecKeyDataTypeSymmetric = 0x0004 - xmlSecKeyDataTypeSession = 0x0008 - xmlSecKeyDataTypePermanent = 0x0010 - xmlSecKeyDataTypeTrusted = 0x0100 - xmlSecKeyDataTypeAny = 0xFFFF - - ctypedef void* xmlSecKeyPtr - - cdef struct _xmlSecKeyReq: - pass - - ctypedef _xmlSecKeyReq xmlSecKeyReq - ctypedef xmlSecKeyReq* xmlSecKeyReqPtr - - void xmlSecKeyDestroy(xmlSecKeyPtr) nogil - - xmlSecKeyPtr xmlSecKeyDuplicate(xmlSecKeyPtr) nogil - - xmlSecKeyPtr xmlSecCryptoAppKeyLoad( - const_char*, xmlSecKeyDataFormat, const_char*, void*, void *) nogil - - xmlSecKeyPtr xmlSecCryptoAppKeyLoadMemory( - const_unsigned_char*, int, xmlSecKeyDataFormat, - const_char*, void*, void*) nogil - - xmlSecKeyPtr xmlSecKeyReadBinaryFile(xmlSecKeyDataId, const_char*) nogil - - # xmlSecKeyPtr xmlSecKeyReadMemory( - # xmlSecKeyDataId, const_unsigned_char*, size_t) nogil - - int xmlSecCryptoAppKeyCertLoad( - xmlSecKeyPtr, const_char*, xmlSecKeyDataFormat) nogil - - int xmlSecCryptoAppKeyCertLoadMemory( - xmlSecKeyPtr, const_unsigned_char*, int, xmlSecKeyDataFormat) nogil - - xmlSecKeyPtr xmlSecKeyGenerate( - xmlSecKeyDataId, size_t, xmlSecKeyDataType) nogil - - int xmlSecKeySetName(xmlSecKeyPtr, const_xmlChar*) nogil - - const_xmlChar* xmlSecKeyGetName(xmlSecKeyPtr) nogil - - int xmlSecKeyMatch(xmlSecKeyPtr, const_xmlChar *, xmlSecKeyReqPtr) nogil - - ctypedef void *xmlSecKeysMngrPtr - - xmlSecKeysMngrPtr xmlSecKeysMngrCreate() nogil - - void xmlSecKeysMngrDestroy(xmlSecKeysMngrPtr) nogil - - int xmlSecCryptoAppDefaultKeysMngrInit(xmlSecKeysMngrPtr) nogil - - int xmlSecCryptoAppDefaultKeysMngrAdoptKey(xmlSecKeysMngrPtr, xmlSecKeyPtr) nogil - - int xmlSecCryptoAppKeysMngrCertLoad( - xmlSecKeysMngrPtr, char * filename, xmlSecKeyDataFormat, xmlSecKeyDataType) nogil - - int xmlSecCryptoAppKeysMngrCertLoadMemory( - xmlSecKeysMngrPtr, const_unsigned_char *, size_t, xmlSecKeyDataFormat, xmlSecKeyDataType) nogil - - -cdef class _KeyData(object): - cdef xmlSecKeyDataId target - - -cdef class Key(object): - cdef xmlSecKeyPtr _handle - cdef bint _owner - - -cdef class KeysManager(object): - cdef xmlSecKeysMngrPtr _handle diff --git a/src/xmlsec/key.pyx b/src/xmlsec/key.pyx deleted file mode 100644 index ae660be1..00000000 --- a/src/xmlsec/key.pyx +++ /dev/null @@ -1,286 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division - -from .key cimport * -from .utils cimport * -from .error import * -from copy import copy - -__all__ = [ - 'KeyData', - 'KeyDataType', - 'KeyFormat', - 'Key', - 'KeysManager' -] - - -class KeyFormat: - UNKNOWN = xmlSecKeyDataFormatUnknown - BINARY = xmlSecKeyDataFormatBinary - PEM = xmlSecKeyDataFormatPem - DER = xmlSecKeyDataFormatDer - PKCS8_PEM = xmlSecKeyDataFormatPkcs8Pem - PKCS8_DER = xmlSecKeyDataFormatPkcs8Der - PKCS12_PEM = xmlSecKeyDataFormatPkcs12 - CERT_PEM = xmlSecKeyDataFormatCertPem - CERT_DER = xmlSecKeyDataFormatCertDer - - -cdef class _KeyData(object): - property name: - def __get__(self): - return _u(self.id.name) - - property href: - def __get__(self): - return _u(self.id.href) - - -cdef _KeyData _mkkdi(xmlSecKeyDataId target): - cdef _KeyData r = _KeyData.__new__(_KeyData) - r.target = target - return r - - -cdef class KeyData(object): - NAME = _mkkdi(xmlSecKeyDataNameId) - VALUE = _mkkdi(xmlSecKeyDataValueId) - RETRIEVALMETHOD = _mkkdi(xmlSecKeyDataRetrievalMethodId) - ENCRYPTEDKEY = _mkkdi(xmlSecKeyDataEncryptedKeyId) - AES = _mkkdi(xmlSecKeyDataAesId) - DES = _mkkdi(xmlSecKeyDataDesId) - DSA = _mkkdi(xmlSecKeyDataDsaId) - ECDSA = _mkkdi(xmlSecKeyDataEcdsaId) - HMAC = _mkkdi(xmlSecKeyDataHmacId) - RSA = _mkkdi(xmlSecKeyDataRsaId) - X509 = _mkkdi(xmlSecKeyDataX509Id) - RAWX509CERT = _mkkdi(xmlSecKeyDataRawX509CertId) - - - -cdef class KeyDataType(object): - UNKNOWN = xmlSecKeyDataTypeUnknown - NONE = xmlSecKeyDataTypeNone - PUBLIC = xmlSecKeyDataTypePublic - PRIVATE = xmlSecKeyDataTypePrivate - SYMMETRIC = xmlSecKeyDataTypeSymmetric - SESSION = xmlSecKeyDataTypeSession - PERMANENT = xmlSecKeyDataTypePermanent - TRUSTED = xmlSecKeyDataTypeTrusted - ANY = xmlSecKeyDataTypeAny - - -cdef class Key(object): - - def __dealloc__(self): - if self._owner and self._handle != NULL: - xmlSecKeyDestroy(self._handle) - - def __deepcopy__(self): - return self.__copy__() - - def __copy__(self): - cdef Key instance = type(self)() - instance._handle = xmlSecKeyDuplicate(self._handle) - if instance._handle == NULL: - raise InternalError("failed to duplicate key", -1) - - return instance - - @classmethod - def from_memory(cls, data, xmlSecKeyDataFormat format, password=None): - """Load PKI key from memory. - """ - - cdef xmlSecKeyPtr handle - cdef size_t c_size - cdef const_unsigned_char *c_data - cdef const_char* c_password = _b(password) - cdef Key instance - - if hasattr(data, "read"): - data = data.read() - - if isinstance(data, str): - data = data.encode('utf8') - - c_size = len(data) - c_data = data - - with nogil: - handle = xmlSecCryptoAppKeyLoadMemory( - c_data, c_size, format, c_password, NULL, NULL) - - if handle == NULL: - raise ValueError("failed to load key") - - # Construct and return a new instance. - instance = cls() - instance._handle = handle - return instance - - @classmethod - def from_file(cls, filename, xmlSecKeyDataFormat format, password=None): - """Load PKI key from a file. - """ - - cdef xmlSecKeyPtr handle - cdef const_char* c_filename = _b(filename) - cdef const_char* c_password = _b(password) - cdef Key instance - - with nogil: - handle = xmlSecCryptoAppKeyLoad( - c_filename, format, c_password, NULL, NULL) - - if handle == NULL: - raise ValueError("failed to load key from '%s'" % filename) - - # Construct and return a new instance. - instance = cls() - instance._handle = handle - return instance - - @classmethod - def generate(cls, _KeyData data, size_t size, xmlSecKeyDataType type): - """Generate key of kind *data* with *size* and *type*. - """ - - cdef xmlSecKeyPtr handle - cdef xmlSecKeyDataId data_id = data.target - cdef Key instance - - with nogil: - handle = xmlSecKeyGenerate(data_id, size, type) - - if handle == NULL: - raise ValueError("failed to generate key") - - # Construct and return a new instance. - instance = cls() - instance._handle = handle - return instance - - @classmethod - def from_binary_file(cls, _KeyData data, filename): - """load (symmetric) key from file. - load key of kind *data* from *filename* - """ - cdef xmlSecKeyPtr handle - cdef const_char* c_filename = _b(filename) - cdef xmlSecKeyDataId data_id = data.target - cdef Key instance - - with nogil: - handle = xmlSecKeyReadBinaryFile(data_id, c_filename) - - if handle == NULL: - raise ValueError("failed to load from '%s'" % filename) - - # Construct and return a new instance. - instance = cls() - instance._handle = handle - return instance - - - def load_cert_from_memory(self, data, xmlSecKeyDataFormat format): - cdef int rv - cdef size_t c_size - cdef const_unsigned_char *c_data - - if isinstance(data, str): - data = data.encode('utf8') - c_size = len(data) - c_data = data - - with nogil: - rv = xmlSecCryptoAppKeyCertLoadMemory( - self._handle, c_data, c_size, format) - - if rv != 0: - raise ValueError('Failed to load the certificate from the I/O stream.') - - def load_cert_from_file(self, filename, xmlSecKeyDataFormat format): - cdef int rv - cdef const_char* c_filename = _b(filename) - cdef Key instance - - with nogil: - rv = xmlSecCryptoAppKeyCertLoad(self._handle, c_filename, format) - - if rv != 0: - raise ValueError('Failed to load the certificate from the file.') - - property name: - def __get__(self): - return _u(xmlSecKeyGetName(self._handle)) - - def __set__(self, value): - xmlSecKeySetName(self._handle, _b(value)) - - -cdef class KeysManager(object): - def __cinit__(self): - cdef int rv - cdef xmlSecKeysMngrPtr handle - - handle = xmlSecKeysMngrCreate() - if handle == NULL: - raise InternalError("failed to create keys manager", -1) - - rv = xmlSecCryptoAppDefaultKeysMngrInit(handle) - if rv < 0: - raise InternalError("failed to initialize keys manager", rv) - self._handle = handle - - def __dealloc__(self): - if self._handle != NULL: - xmlSecKeysMngrDestroy(self._handle) - - def add_key(self, Key key): - """add (a copy of) *key*.""" - - cdef int rv - cdef xmlSecKeyPtr key_handle = xmlSecKeyDuplicate(key._handle) - if key_handle == NULL: - raise InternalError("failed to copy key", -1) - - rv = xmlSecCryptoAppDefaultKeysMngrAdoptKey(self._handle, key_handle) - if rv < 0: - xmlSecKeyDestroy(key_handle) - raise Error("failed to add key", rv) - - def load_cert(self, filename, xmlSecKeyDataFormat format, xmlSecKeyDataType type): - """load certificate from *filename*. - *format* specifies the key data format. - *type* specifies the type and is an or of `KeyDataType*` constants. - """ - cdef int rv - cdef const_char* c_filename = _b(filename) - - with nogil: - rv = xmlSecCryptoAppKeysMngrCertLoad(self._handle, c_filename, format, type) - - if rv != 0: - raise Error("failed to load certificate from '%s'" % filename, rv) - - def load_cert_from_memory(self, data, xmlSecKeyDataFormat format, xmlSecKeyDataType type): - """load certificate from *data* (a sequence of bytes). - *format* specifies the key_data_format. - *type* specifies the type and is an or of `KeyDataType*` constants. - """ - cdef int rv - cdef size_t c_size - cdef const_unsigned_char *c_data - - if isinstance(data, str): - data = data.encode('utf8') - c_size = len(data) - c_data = data - - with nogil: - rv = xmlSecCryptoAppKeysMngrCertLoadMemory(self._handle, c_data, c_size, format, type) - - if rv != 0: - raise Error("failed to load certificate from memory", rv) diff --git a/src/xmlsec/meta.py b/src/xmlsec/meta.py deleted file mode 100644 index 91e6d3c5..00000000 --- a/src/xmlsec/meta.py +++ /dev/null @@ -1,2 +0,0 @@ -version = '0.3.1' -description = 'Python bindings for the XML Security Library.' 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.pxd b/src/xmlsec/template.pxd deleted file mode 100644 index cdc3b183..00000000 --- a/src/xmlsec/template.pxd +++ /dev/null @@ -1,37 +0,0 @@ -from lxml.includes.tree cimport const_xmlChar, xmlNode, xmlDoc -from .constants cimport xmlSecTransformId - - -cdef extern from "xmlsec.h": # xmlsec/templates.h - - xmlNode* xmlSecTmplSignatureCreateNsPref( - xmlDoc* document, xmlSecTransformId c14n, xmlSecTransformId sign, - const_xmlChar* id, const_xmlChar* ns) nogil - - xmlNode* xmlSecTmplSignatureAddReference( - xmlNode* node, xmlSecTransformId digest, - const_xmlChar* id, const_xmlChar* uri, const_xmlChar* type) nogil - - xmlNode* xmlSecTmplReferenceAddTransform( - xmlNode* node, xmlSecTransformId transform) nogil - - xmlNode* xmlSecTmplSignatureEnsureKeyInfo( - xmlNode* node, const_xmlChar* id) nogil - - xmlNode* xmlSecTmplKeyInfoAddKeyName(xmlNode* node, const_xmlChar* name) nogil - - xmlNode* xmlSecTmplKeyInfoAddKeyValue(xmlNode* node) nogil - - xmlNode* xmlSecTmplKeyInfoAddX509Data(xmlNode* node) nogil - - xmlNode* xmlSecTmplKeyInfoAddEncryptedKey( - xmlNode* keyInfoNode, xmlSecTransformId encMethodId, - const_xmlChar *id, const_xmlChar *type, const_xmlChar *recipient) nogil - - xmlNode* xmlSecTmplEncDataCreate( - xmlDoc* doc, xmlSecTransformId encMethodId, const_xmlChar *id, - const_xmlChar *type, const_xmlChar *mimeType, const_xmlChar *encoding) nogil - - xmlNode* xmlSecTmplEncDataEnsureKeyInfo(xmlNode* encNode, const_xmlChar *id) nogil - - xmlNode* xmlSecTmplEncDataEnsureCipherValue(xmlNode* encNode) nogil 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/template.pyx b/src/xmlsec/template.pyx deleted file mode 100644 index 81435e9e..00000000 --- a/src/xmlsec/template.pyx +++ /dev/null @@ -1,152 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division - -from lxml.includes.etreepublic cimport import_lxml__etree -import_lxml__etree() - -from lxml.includes.etreepublic cimport _Element, elementFactory -from lxml.includes.tree cimport const_xmlChar, xmlNode, xmlStrdup -from .constants cimport _Transform -from .utils cimport * -from .template cimport * - - -def create(_Element node not None, - _Transform c14n_method not None, - _Transform sign_method not None, - name=None, - ns=None): - - cdef xmlNode* c_node - cdef const_xmlChar* c_name = _b(name) - cdef const_xmlChar* c_ns = _b(ns) - - c_node = xmlSecTmplSignatureCreateNsPref( - node._doc._c_doc, c14n_method.target, sign_method.target, c_name, c_ns) - - return elementFactory(node._doc, c_node) - - -def add_reference(_Element node not None, - _Transform digest_method not None, - id=None, uri=None, type=None): - - cdef xmlNode* c_node - cdef const_xmlChar* c_id = _b(id) - cdef const_xmlChar* c_uri = _b(uri) - cdef const_xmlChar* c_type = _b(type) - - c_node = xmlSecTmplSignatureAddReference( - node._c_node, digest_method.target, c_id, c_uri, c_type) - - return elementFactory(node._doc, c_node) - - -def add_transform(_Element node not None, _Transform transform not None): - cdef xmlNode* c_node - - c_node = xmlSecTmplReferenceAddTransform( - node._c_node, transform.target) - - return elementFactory(node._doc, c_node) - - -def ensure_key_info(_Element node not None, id=None): - - cdef xmlNode* c_node - cdef const_xmlChar* c_id = _b(id) - - c_node = xmlSecTmplSignatureEnsureKeyInfo(node._c_node, c_id) - - return elementFactory(node._doc, c_node) - - -def add_key_name(_Element node not None, name=None): - - cdef xmlNode* c_node - cdef const_xmlChar* c_name = _b(name) - - c_node = xmlSecTmplKeyInfoAddKeyName(node._c_node, c_name) - - return elementFactory(node._doc, c_node) - - -def add_key_value(_Element node not None): - - cdef xmlNode* c_node - - c_node = xmlSecTmplKeyInfoAddKeyValue(node._c_node) - - return elementFactory(node._doc, c_node) - - -def add_x509_data(_Element node not None): - - cdef xmlNode* c_node - - c_node = xmlSecTmplKeyInfoAddX509Data(node._c_node) - - return elementFactory(node._doc, c_node) - - -def add_encrypted_key(_Element node not None, - _Transform method not None, - id=None, - type=None, - recipient=None): - """Adds node with given attributes to the node keyInfoNode. - """ - - cdef xmlNode* c_node - cdef const_xmlChar* c_id = _b(id) - cdef const_xmlChar* c_type = _b(type) - cdef const_xmlChar* c_recipient = _b(recipient) - - c_node = xmlSecTmplKeyInfoAddEncryptedKey(node._c_node, method.target, c_id, c_type, c_recipient) - return elementFactory(node._doc, c_node) - - -def encrypted_data_create(_Element node not None, - _Transform method not None, - id=None, - type=None, - mime_type=None, - encoding=None, - ns=None): - """ - Creates new <{ns}:EncryptedData /> node for encryption template. - """ - cdef xmlNode* c_node - cdef const_xmlChar* c_id = _b(id) - cdef const_xmlChar* c_type = _b(type) - cdef const_xmlChar* c_mtype = _b(mime_type) - cdef const_xmlChar* c_encoding = _b(encoding) - - c_node = xmlSecTmplEncDataCreate( - node._doc._c_doc, method.target, c_id, c_type, c_mtype, c_encoding) - - if ns is not None: - c_node.ns.prefix = xmlStrdup(_b(ns)) - return elementFactory(node._doc, c_node) - - -def encrypted_data_ensure_key_info(_Element node not None, id=None, ns=None): - """ - Adds <{ns}:KeyInfo/> to the node encNode. - """ - - cdef xmlNode* c_node - cdef const_xmlChar* c_id = _b(id) - - c_node = xmlSecTmplEncDataEnsureKeyInfo(node._c_node, c_id) - if ns is not None: - c_node.ns.prefix = xmlStrdup(_b(ns)) - - return elementFactory(node._doc, c_node) - - -def encrypted_data_ensure_cipher_value(_Element node not None): - cdef xmlNode* c_node - c_node = xmlSecTmplEncDataEnsureCipherValue(node._c_node) - - return elementFactory(node._doc, c_node) diff --git a/src/xmlsec/tree.pxd b/src/xmlsec/tree.pxd deleted file mode 100644 index 5ed86d6e..00000000 --- a/src/xmlsec/tree.pxd +++ /dev/null @@ -1,68 +0,0 @@ -from lxml.includes.tree cimport const_xmlChar, xmlNode, xmlDoc - - -cdef extern from "xmlsec.h": # xmlsec/xmltree.h - - # const_xmlChar* xmlSecGetNodeNsHref(xmlNode* node) - - # bint xmlSecCheckNodeName( - # xmlNode* node, const_xmlChar* name, const_xmlChar* ns) - - # xmlNode* xmlSecGetNextElementNode(xmlNode* node) - - xmlNode* xmlSecFindChild( - xmlNode* parent, const_xmlChar* name, const_xmlChar* ns) - - xmlNode* xmlSecFindParent( - xmlNode* node, const_xmlChar* name, const_xmlChar* ns) - - xmlNode* xmlSecFindNode( - xmlNode* parent, const_xmlChar* name, const_xmlChar* ns) - - # xmlNode* xmlSecAddChild( - # xmlNode* parent, const_xmlChar* name, const_xmlChar* ns) - - # xmlNode* xmlSecAddChildNode(xmlNode* parent, xmlNode* child) - - # xmlNode* xmlSecAddNextSibling( - # xmlNode* node, const_xmlChar* name, const_xmlChar* ns) - - # xmlNode* xmlSecAddPrevSibling( - # xmlNode* node, const_xmlChar* name, const_xmlChar* ns) - - # int xmlSecReplaceNode(xmlNode* node, xmlNode* new_node) - - # int xmlSecReplaceNodeAndReturn( - # xmlNode* node, xmlNode* new_node, xmlNode** replaced) - - # int xmlSecReplaceContent(xmlNode* node, xmlNode* new_node) - - # int xmlSecReplaceContentAndReturn( - # xmlNode* node, xmlNode* new_node, xmlNode** replaced) - - # int xmlSecReplaceNodeBuffer( - # xmlNode* node, const_xmlSecByte* buffer, xmlSecSize size) - - # int xmlSecReplaceNodeBufferAndReturn( - # xmlNode* node, const_xmlSecByte* buffer, xmlSecSize size, - # xmlNode** replaced) - - # int xmlSecNodeEncodeAndSetContent(xmlNode* node, const_xmlChar* buffer) - - int xmlSecAddIDs(xmlDoc* document, xmlNode* node, const_xmlChar** ids) nogil - - # int xmlSecGenerateAndAddID( - # xmlNode* node, const_xmlChar* name, const_xmlChar* prefix) - - # xmlChar* xmlSecGenerateID(const_xmlChar* prefix, xmlSecSize length) - - # xmlDoc* xmlSecCreateTree(const_xmlChar* name, const_xmlChar* ns) - - # bint xmlSecIsEmptyNode(xmlNode* node) - - # bint xmlSecIsEmptyString(const_xmlChar* text) - - # xmlChar* xmlSecGetQName( - # xmlNode* node, const_xmlChar* href, const_xmlChar* local) - - # int xmlSecPrintXmlString(FILE*, const_xmlChar* text) 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/src/xmlsec/tree.pyx b/src/xmlsec/tree.pyx deleted file mode 100644 index 2cf7557d..00000000 --- a/src/xmlsec/tree.pyx +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division - -from libc cimport stdlib -from lxml.includes.etreepublic cimport import_lxml__etree -import_lxml__etree() - -from lxml.includes.etreepublic cimport _ElementTree, _Element, elementFactory -from .tree cimport * -from .utils cimport * -from .constants import Namespace - -__all__ = [ - 'find_child', - 'find_parent', - 'find_node', - 'add_ids' -] - - -def find_child(_Element parent not None, - name not None, - namespace=Namespace.DS): - """ - Searches a direct child of the parent node having given name and - namespace href. - """ - cdef xmlNode* c_node - c_node = xmlSecFindChild(parent._c_node, _b(name), _b(namespace)) - return elementFactory(parent._doc, c_node) if c_node else None - - -def find_parent(_Element node not None, - name not None, - namespace=Namespace.DS): - """ - Searches the ancestors axis of the node for a node having given name - and namespace href. - """ - cdef xmlNode* c_node - c_node = xmlSecFindParent(node._c_node, _b(name), _b(namespace)) - return elementFactory(node._doc, c_node) if c_node else None - - -def find_node(_Element parent not None, - name not None, - namespace=Namespace.DS): - """ - Searches all children of the parent node having given name and - namespace href. - """ - cdef xmlNode* c_node - c_node = xmlSecFindNode(parent._c_node, _b(name), _b(namespace)) - return elementFactory(parent._doc, c_node) if c_node else None - - - -def add_ids(_Element node, ids): - """register *ids* as ids used below *node*. - *ids* is a sequence of attribute names used as XML ids in the subtree - rooted at *node*. - A call to `addIds` may be necessary to make known which attributes - contain XML ids. 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). - """ - - cdef const_xmlChar **lst - cdef int i, n - - n = len(ids) - lst = stdlib.malloc(sizeof(const_xmlChar*) * (n + 1)) - if lst == NULL: - raise MemoryError - try: - for i in range(n): - lst[i] = _b(ids[i]) - lst[n] = NULL - with nogil: - xmlSecAddIDs(node._doc._c_doc, node._c_node, lst) - finally: - stdlib.free(lst) diff --git a/src/xmlsec/utils.pxd b/src/xmlsec/utils.pxd deleted file mode 100644 index f5a2e878..00000000 --- a/src/xmlsec/utils.pxd +++ /dev/null @@ -1,33 +0,0 @@ -from lxml.includes.tree cimport const_xmlChar - - -cdef inline const_xmlChar* _b(text): - if text is None: - return NULL - - if isinstance(text, str): - text = text.encode('utf8') - return text - - return text - - -cdef inline _u(const_xmlChar* text): - return text.decode('utf8') if text != NULL else None - - -cdef extern from "xmlsec.h": # xmlsec/xmlsec.h - int xmlSecInit() nogil - int xmlSecShutdown() nogil - - -cdef extern from "xmlsec.h": # xmlsec/errors.h - int xmlSecErrorsDefaultCallbackEnableOutput(int) nogil - - -cdef extern from "xmlsec.h": # xmlsec/app.h - int xmlSecCryptoInit() nogil - int xmlSecCryptoShutdown() nogil - - int xmlSecCryptoAppInit(char* name) nogil - int xmlSecCryptoAppShutdown() nogil diff --git a/src/xmlsec/utils.pyx b/src/xmlsec/utils.pyx deleted file mode 100644 index 8287bad1..00000000 --- a/src/xmlsec/utils.pyx +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division - -from .utils cimport * - -__all__ = [ - 'init', - 'shutdown', - 'enable_debug_trace' -] - - -def init(): - """Initialize the library for general operation. - - This is called upon library import and does not need to be called - again (unless @ref _shutdown is called explicitly). - """ - r = xmlSecInit() - if r != 0: - return False - - r = xmlSecCryptoInit() - if r != 0: - return False - - r = xmlSecCryptoAppInit(NULL) - if r != 0: - return False - - return True - - -def shutdown(): - """Shutdown the library and cleanup any leftover resources. - - This is called automatically upon interpreter termination and - should not need to be called explicitly. - """ - r = xmlSecCryptoAppShutdown() - if r != 0: - return False - - r = xmlSecCryptoShutdown() - if r != 0: - return False - - r = xmlSecShutdown() - return r == 0 - - -def enable_debug_trace(flag): - xmlSecErrorsDefaultCallbackEnableOutput(flag) 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/data/sign4-in.xml b/tests/data/sign4-in.xml new file mode 100644 index 00000000..d49fc3ae --- /dev/null +++ b/tests/data/sign4-in.xml @@ -0,0 +1,9 @@ + + + + + Hello, World! + + diff --git a/tests/data/sign4-out.xml b/tests/data/sign4-out.xml new file mode 100644 index 00000000..bdd1014e --- /dev/null +++ b/tests/data/sign4-out.xml @@ -0,0 +1,55 @@ + + + Hello, World! + + + + + + + + + + + +ERS7F/ifxZoyTe0mhco+HeAEKGo= + + +G+mZNFqh9w0wIkDSbuYwvVDu7CMP8PEsw7jfwiZBC8nyF3loAtYKKAkdi6Zy3dJs +tU8qKfhzvabmCjdIrGkFTdtlCNCVKDMzwogFtxEX4Oh77X6jjx4b22XNJx4AbnUG +JV/EcsD+po8s5qVEXw62lRRd8cMDafbzOA/rBH96CMNgZhzxyaF9VRLa/vbt1ht2 +hE1KkdZCB4Y0Lv3QyeDL2jax3NFks9FUv8IqoWYQSvywdMLY2ZMiQ9UpPeVfMizi +trd5zDUSD/s3hyIEs4gD5NJF3HZPD/Fe2Zw1PBPj9eLADdEzcdueyCdPJ2YioFIi +1sMW/qPDhR/DoOJwGpUxwQ== + + +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/data/sign5-in.xml b/tests/data/sign5-in.xml new file mode 100644 index 00000000..7676fffe --- /dev/null +++ b/tests/data/sign5-in.xml @@ -0,0 +1,9 @@ + + + + + Hello, World! + + 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/data/sign5-out.xml b/tests/data/sign5-out.xml new file mode 100644 index 00000000..4d8d28be --- /dev/null +++ b/tests/data/sign5-out.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 +emailAddress=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Examples RSA Certificate,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US +JIQs8tRZIGKLLlyGkKOqMLonGpw= + + + 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 6415dbb4..00000000 --- a/tests/examples/base.py +++ /dev/null @@ -1,21 +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 d9f45731..00000000 --- a/tests/examples/test_decrypt.py +++ /dev/null @@ -1,37 +0,0 @@ -from os import path -import xmlsec -from .base import parse_xml, BASE_DIR, compare - - -def test_decrypt1(): - manager = xmlsec.KeysManager() - filename = path.join(BASE_DIR, 'rsakey.pem') - key = xmlsec.Key.from_memory(open(filename, 'rb'), 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(open(filename, 'rb'), 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.constants.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 54b5d334..00000000 --- a/tests/examples/test_encrypt.py +++ /dev/null @@ -1,80 +0,0 @@ -from os import path -import xmlsec -from .base import parse_xml, BASE_DIR -from lxml import etree - - -def test_encrypt_xml(): - # Load the public cert - manager = xmlsec.KeysManager() - filename = path.join(BASE_DIR, 'rsacert.pem') - key = xmlsec.Key.from_memory(open(filename, 'rb').read(), 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_data = 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(open(filename, 'rb').read(), 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 c3041b32..00000000 --- a/tests/examples/test_sign.py +++ /dev/null @@ -1,178 +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_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 4d37fd7d..00000000 --- a/tests/examples/test_verify.py +++ /dev/null @@ -1,59 +0,0 @@ -from os import path -from pytest import mark -import xmlsec -from .base import parse_xml, BASE_DIR - - -@mark.parametrize('index', range(1, 4)) -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) - - # 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) 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()